mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
chore(editor): Refactoring instance-ai workflow setup FE code (no-changelog) (#30012)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Charlie Kolb <charlie@n8n.io>
This commit is contained in:
parent
2e046d5b7f
commit
3df6611fb3
|
|
@ -250,6 +250,17 @@ export const workflowSetupNodeSchema = z.object({
|
|||
'Whether this node still requires user intervention. ' +
|
||||
'False when credentials are set and valid, parameters are resolved, etc.',
|
||||
),
|
||||
subnodeRootNode: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
typeVersion: z.number(),
|
||||
id: z.string(),
|
||||
})
|
||||
.optional()
|
||||
.describe(
|
||||
'Snapshot of the root node for this sub-node connected via a non-Main port (e.g. ai_languageModel, ai_memory, ai_tool). Carries the metadata needed to render the group header even when the root node itself has no setup request.',
|
||||
),
|
||||
});
|
||||
export type InstanceAiWorkflowSetupNode = z.infer<typeof workflowSetupNodeSchema>;
|
||||
|
||||
|
|
|
|||
|
|
@ -562,6 +562,42 @@ describe('credentials tool', () => {
|
|||
expect(suspendFn.mock.calls[0][0]).toEqual(expect.objectContaining({ projectId: 'proj-1' }));
|
||||
});
|
||||
|
||||
it('should scope credential lookup to projectId when provided', async () => {
|
||||
const context = createMockContext();
|
||||
(context.credentialService.list as jest.Mock).mockResolvedValue([]);
|
||||
|
||||
const tool = createCredentialsTool(context);
|
||||
await tool.execute!(
|
||||
{
|
||||
action: 'setup' as const,
|
||||
credentials: [{ credentialType: 'slackApi' }],
|
||||
projectId: 'proj-1',
|
||||
},
|
||||
suspendCtx(jest.fn()),
|
||||
);
|
||||
|
||||
expect(context.credentialService.list).toHaveBeenCalledWith({
|
||||
type: 'slackApi',
|
||||
projectId: 'proj-1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should omit projectId from credential lookup when not provided', async () => {
|
||||
const context = createMockContext();
|
||||
(context.credentialService.list as jest.Mock).mockResolvedValue([]);
|
||||
|
||||
const tool = createCredentialsTool(context);
|
||||
await tool.execute!(
|
||||
{
|
||||
action: 'setup' as const,
|
||||
credentials: [{ credentialType: 'slackApi' }],
|
||||
},
|
||||
suspendCtx(jest.fn()),
|
||||
);
|
||||
|
||||
expect(context.credentialService.list).toHaveBeenCalledWith({ type: 'slackApi' });
|
||||
});
|
||||
|
||||
it('should include credentialFlow in suspend payload for finalize stage', async () => {
|
||||
const context = createMockContext();
|
||||
(context.credentialService.list as jest.Mock).mockResolvedValue([]);
|
||||
|
|
|
|||
|
|
@ -543,11 +543,18 @@ describe('workflows tool', () => {
|
|||
|
||||
describe('setup action', () => {
|
||||
it('should analyze workflow and suspend for user setup', async () => {
|
||||
const actionableSetupRequest = {
|
||||
node: { name: 'Slack', type: 'n8n-nodes-base.slack' },
|
||||
credentialType: 'slackApi',
|
||||
needsAction: true,
|
||||
};
|
||||
const setupRequests = [
|
||||
actionableSetupRequest,
|
||||
{
|
||||
node: { name: 'Slack', type: 'n8n-nodes-base.slack' },
|
||||
credentialType: 'slackApi',
|
||||
needsAction: true,
|
||||
node: { name: 'Webhook', type: 'n8n-nodes-base.webhook' },
|
||||
isTrigger: true,
|
||||
isTestable: true,
|
||||
needsAction: false,
|
||||
},
|
||||
];
|
||||
(analyzeWorkflow as jest.Mock).mockResolvedValue(setupRequests);
|
||||
|
|
@ -565,11 +572,33 @@ describe('workflows tool', () => {
|
|||
expect(suspend.mock.calls[0][0]).toMatchObject({
|
||||
message: 'Configure credentials for your workflow',
|
||||
severity: 'info',
|
||||
setupRequests,
|
||||
setupRequests: [actionableSetupRequest],
|
||||
workflowId: 'wf1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return success when setup analysis only has non-actionable requests', async () => {
|
||||
(analyzeWorkflow as jest.Mock).mockResolvedValue([
|
||||
{
|
||||
node: { name: 'Webhook', type: 'n8n-nodes-base.webhook' },
|
||||
isTrigger: true,
|
||||
isTestable: true,
|
||||
needsAction: false,
|
||||
},
|
||||
]);
|
||||
|
||||
const context = createMockContext();
|
||||
const suspend = jest.fn();
|
||||
|
||||
const tool = createWorkflowsTool(context, 'full');
|
||||
const result = await tool.execute!({ action: 'setup', workflowId: 'wf1' }, {
|
||||
agent: { suspend, resumeData: undefined },
|
||||
} as never);
|
||||
|
||||
expect(result).toEqual({ success: true, reason: 'No nodes require setup.' });
|
||||
expect(suspend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return success when no nodes need setup', async () => {
|
||||
(analyzeWorkflow as jest.Mock).mockResolvedValue([]);
|
||||
|
||||
|
|
|
|||
|
|
@ -259,12 +259,17 @@ async function handleSetup(
|
|||
const suspend = ctx?.agent?.suspend as ((payload: unknown) => Promise<void>) | undefined;
|
||||
const isFinalize = input.credentialFlow?.stage === 'finalize';
|
||||
|
||||
// State 1: First call — look up existing credentials per type and suspend
|
||||
// State 1: First call — look up existing credentials per type and suspend.
|
||||
// Scope the lookup to `projectId` when provided so the candidates match what
|
||||
// the workflow being built can actually use.
|
||||
if (resumeData === undefined || resumeData === null) {
|
||||
const credentialRequests = await Promise.all(
|
||||
input.credentials.map(
|
||||
async (req: { credentialType: string; reason?: string; suggestedName?: string }) => {
|
||||
const existing = await context.credentialService.list({ type: req.credentialType });
|
||||
const existing = await context.credentialService.list({
|
||||
type: req.credentialType,
|
||||
...(input.projectId ? { projectId: input.projectId } : {}),
|
||||
});
|
||||
return {
|
||||
credentialType: req.credentialType,
|
||||
reason: req.reason ?? `Required for ${req.credentialType}`,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ import { z } from 'zod';
|
|||
import { sanitizeInputSchema } from '../agent/sanitize-mcp-schemas';
|
||||
import type { InstanceAiContext } from '../types';
|
||||
import { formatTimestamp } from '../utils/format-timestamp';
|
||||
import { setupSuspendSchema, setupResumeSchema } from './workflows/setup-workflow.schema';
|
||||
import {
|
||||
setupSuspendSchema,
|
||||
setupResumeSchema,
|
||||
type SetupRequest,
|
||||
} from './workflows/setup-workflow.schema';
|
||||
import {
|
||||
analyzeWorkflow,
|
||||
applyNodeChanges,
|
||||
|
|
@ -287,6 +291,13 @@ async function handleUnarchive(
|
|||
return { success: true };
|
||||
}
|
||||
|
||||
function isActionableSetupRequest(req: SetupRequest): boolean {
|
||||
return (
|
||||
req.needsAction === true &&
|
||||
(req.credentialType !== undefined || (req.editableParameters?.length ?? 0) > 0)
|
||||
);
|
||||
}
|
||||
|
||||
async function handleSetup(
|
||||
context: InstanceAiContext,
|
||||
input: Extract<Input, { action: 'setup' }>,
|
||||
|
|
@ -305,7 +316,9 @@ async function handleSetup(
|
|||
|
||||
// State 1: Analyze workflow and suspend for user setup
|
||||
if (resumeData === undefined || resumeData === null) {
|
||||
const setupRequests = await analyzeWorkflow(context, input.workflowId);
|
||||
const setupRequests = (await analyzeWorkflow(context, input.workflowId)).filter(
|
||||
isActionableSetupRequest,
|
||||
);
|
||||
|
||||
if (setupRequests.length === 0) {
|
||||
return { success: true, reason: 'No nodes require setup.' };
|
||||
|
|
|
|||
|
|
@ -377,6 +377,45 @@ describe('buildSetupRequests', () => {
|
|||
expect(context.credentialService.list).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('forwards workflowId to credentialService.list so candidates match save-time scope', async () => {
|
||||
(context.credentialService.list as jest.Mock).mockResolvedValue([]);
|
||||
|
||||
const node = makeNode();
|
||||
await buildSetupRequests(context, node, undefined, undefined, 'wf-1');
|
||||
|
||||
expect(context.credentialService.list).toHaveBeenCalledWith({
|
||||
type: 'slackApi',
|
||||
workflowId: 'wf-1',
|
||||
});
|
||||
});
|
||||
|
||||
it('omits workflowId from credentialService.list when not provided', async () => {
|
||||
(context.credentialService.list as jest.Mock).mockResolvedValue([]);
|
||||
|
||||
const node = makeNode();
|
||||
await buildSetupRequests(context, node);
|
||||
|
||||
expect(context.credentialService.list).toHaveBeenCalledWith({ type: 'slackApi' });
|
||||
});
|
||||
|
||||
it('cache discriminates by workflowId so a shared cache stays correct across workflows', async () => {
|
||||
(context.credentialService.list as jest.Mock).mockResolvedValue([]);
|
||||
|
||||
const cache = createCredentialCache();
|
||||
const node = makeNode();
|
||||
|
||||
await buildSetupRequests(context, node, undefined, cache, 'wf-1');
|
||||
await buildSetupRequests(context, node, undefined, cache, 'wf-1');
|
||||
expect(context.credentialService.list).toHaveBeenCalledTimes(1);
|
||||
|
||||
await buildSetupRequests(context, node, undefined, cache, 'wf-2');
|
||||
expect(context.credentialService.list).toHaveBeenCalledTimes(2);
|
||||
expect(context.credentialService.list).toHaveBeenLastCalledWith({
|
||||
type: 'slackApi',
|
||||
workflowId: 'wf-2',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not generate credential request for HTTP Request with auth=none and stale node.credentials', async () => {
|
||||
(context.nodeService as unknown as Record<string, unknown>).getNodeCredentialTypes = jest
|
||||
.fn()
|
||||
|
|
@ -697,6 +736,257 @@ describe('analyzeWorkflow', () => {
|
|||
const names = result.map((r) => r.node.name);
|
||||
expect(names.indexOf('Webhook')).toBeLessThan(names.indexOf('Slack'));
|
||||
});
|
||||
|
||||
describe('subnodeRootNode stamping for sub-nodes', () => {
|
||||
it('stamps subnodeRootNode on every sub-node connected to an agent', async () => {
|
||||
const agent = makeNode({
|
||||
name: 'Agent',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 1,
|
||||
id: 'agent-1',
|
||||
});
|
||||
const model = makeNode({
|
||||
name: 'OpenAI Model',
|
||||
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||
typeVersion: 1,
|
||||
id: 'model-1',
|
||||
});
|
||||
const memory = makeNode({
|
||||
name: 'Memory',
|
||||
type: '@n8n/n8n-nodes-langchain.memoryBufferWindow',
|
||||
typeVersion: 1,
|
||||
id: 'memory-1',
|
||||
});
|
||||
(context.workflowService.getAsWorkflowJSON as jest.Mock).mockResolvedValue(
|
||||
makeWorkflowJSON([agent, model, memory], {
|
||||
'OpenAI Model': {
|
||||
ai_languageModel: [[{ node: 'Agent', type: 'ai_languageModel', index: 0 }]],
|
||||
},
|
||||
Memory: { ai_memory: [[{ node: 'Agent', type: 'ai_memory', index: 0 }]] },
|
||||
}),
|
||||
);
|
||||
(context.nodeService.getDescription as jest.Mock).mockImplementation(async (type: string) => {
|
||||
if (type === '@n8n/n8n-nodes-langchain.lmChatOpenAi') {
|
||||
return await Promise.resolve({
|
||||
group: [],
|
||||
credentials: [{ name: 'openAiApi' }],
|
||||
});
|
||||
}
|
||||
if (type === '@n8n/n8n-nodes-langchain.memoryBufferWindow') {
|
||||
return await Promise.resolve({ group: [], credentials: [] });
|
||||
}
|
||||
return await Promise.resolve({ group: [], credentials: [] });
|
||||
});
|
||||
(context.credentialService.list as jest.Mock).mockResolvedValue([]);
|
||||
|
||||
const result = await analyzeWorkflow(context, 'wf-1');
|
||||
|
||||
const modelReq = result.find((r) => r.node.name === 'OpenAI Model');
|
||||
expect(modelReq?.subnodeRootNode).toEqual({
|
||||
name: 'Agent',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 1,
|
||||
id: 'agent-1',
|
||||
});
|
||||
});
|
||||
|
||||
it('stamps the topmost root node for transitively nested sub-agents', async () => {
|
||||
const agent = makeNode({
|
||||
name: 'Agent',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 1,
|
||||
id: 'agent-1',
|
||||
});
|
||||
const tool = makeNode({
|
||||
name: 'Tool',
|
||||
type: '@n8n/n8n-nodes-langchain.toolWorkflow',
|
||||
typeVersion: 1,
|
||||
id: 'tool-1',
|
||||
});
|
||||
const subModel = makeNode({
|
||||
name: 'Sub Model',
|
||||
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||
typeVersion: 1,
|
||||
id: 'sub-model-1',
|
||||
});
|
||||
(context.workflowService.getAsWorkflowJSON as jest.Mock).mockResolvedValue(
|
||||
makeWorkflowJSON([agent, tool, subModel], {
|
||||
Tool: { ai_tool: [[{ node: 'Agent', type: 'ai_tool', index: 0 }]] },
|
||||
'Sub Model': {
|
||||
ai_languageModel: [[{ node: 'Tool', type: 'ai_languageModel', index: 0 }]],
|
||||
},
|
||||
}),
|
||||
);
|
||||
(context.nodeService.getDescription as jest.Mock).mockImplementation(async (type: string) => {
|
||||
if (type === '@n8n/n8n-nodes-langchain.lmChatOpenAi') {
|
||||
return await Promise.resolve({
|
||||
group: [],
|
||||
credentials: [{ name: 'openAiApi' }],
|
||||
});
|
||||
}
|
||||
return await Promise.resolve({ group: [], credentials: [] });
|
||||
});
|
||||
(context.credentialService.list as jest.Mock).mockResolvedValue([]);
|
||||
|
||||
const result = await analyzeWorkflow(context, 'wf-1');
|
||||
|
||||
const subModelReq = result.find((r) => r.node.name === 'Sub Model');
|
||||
expect(subModelReq?.subnodeRootNode?.name).toBe('Agent');
|
||||
});
|
||||
|
||||
it('keeps subnodeRootNode metadata even when the root node itself produced no setup request', async () => {
|
||||
const agent = makeNode({
|
||||
name: 'Agent',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 1,
|
||||
id: 'agent-1',
|
||||
});
|
||||
const model = makeNode({
|
||||
name: 'Model',
|
||||
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||
typeVersion: 1,
|
||||
id: 'model-1',
|
||||
});
|
||||
(context.workflowService.getAsWorkflowJSON as jest.Mock).mockResolvedValue(
|
||||
makeWorkflowJSON([agent, model], {
|
||||
Model: { ai_languageModel: [[{ node: 'Agent', type: 'ai_languageModel', index: 0 }]] },
|
||||
}),
|
||||
);
|
||||
(context.nodeService.getDescription as jest.Mock).mockImplementation(async (type: string) => {
|
||||
if (type === '@n8n/n8n-nodes-langchain.lmChatOpenAi') {
|
||||
return await Promise.resolve({
|
||||
group: [],
|
||||
credentials: [{ name: 'openAiApi' }],
|
||||
});
|
||||
}
|
||||
// Agent itself returns no credentials → no setup request for it.
|
||||
return await Promise.resolve({ group: [], credentials: [] });
|
||||
});
|
||||
(context.credentialService.list as jest.Mock).mockResolvedValue([]);
|
||||
|
||||
const result = await analyzeWorkflow(context, 'wf-1');
|
||||
|
||||
expect(result.find((r) => r.node.name === 'Agent')).toBeUndefined();
|
||||
const modelReq = result.find((r) => r.node.name === 'Model');
|
||||
expect(modelReq?.subnodeRootNode).toEqual({
|
||||
name: 'Agent',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 1,
|
||||
id: 'agent-1',
|
||||
});
|
||||
});
|
||||
|
||||
it('attaches a multi-root sub-node to the first root node in execution order', async () => {
|
||||
const trigger = makeNode({
|
||||
name: 'Trigger',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
id: 'trigger-1',
|
||||
position: [0, 0] as [number, number],
|
||||
});
|
||||
const agentA = makeNode({
|
||||
name: 'Agent A',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 1,
|
||||
id: 'agent-a',
|
||||
position: [200, 0] as [number, number],
|
||||
});
|
||||
const agentB = makeNode({
|
||||
name: 'Agent B',
|
||||
type: '@n8n/n8n-nodes-langchain.agent',
|
||||
typeVersion: 1,
|
||||
id: 'agent-b',
|
||||
position: [400, 0] as [number, number],
|
||||
});
|
||||
const sharedModel = makeNode({
|
||||
name: 'Shared Model',
|
||||
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||
typeVersion: 1,
|
||||
id: 'shared-1',
|
||||
});
|
||||
(context.workflowService.getAsWorkflowJSON as jest.Mock).mockResolvedValue(
|
||||
makeWorkflowJSON([trigger, agentA, agentB, sharedModel], {
|
||||
Trigger: {
|
||||
main: [
|
||||
[
|
||||
{ node: 'Agent A', type: 'main', index: 0 },
|
||||
{ node: 'Agent B', type: 'main', index: 0 },
|
||||
],
|
||||
],
|
||||
},
|
||||
'Shared Model': {
|
||||
ai_languageModel: [
|
||||
[
|
||||
{ node: 'Agent A', type: 'ai_languageModel', index: 0 },
|
||||
{ node: 'Agent B', type: 'ai_languageModel', index: 0 },
|
||||
],
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
(context.nodeService.getDescription as jest.Mock).mockImplementation(async (type: string) => {
|
||||
if (type === 'n8n-nodes-base.webhook') {
|
||||
return await Promise.resolve({
|
||||
group: ['trigger'],
|
||||
credentials: [],
|
||||
webhooks: [{}],
|
||||
});
|
||||
}
|
||||
if (type === '@n8n/n8n-nodes-langchain.lmChatOpenAi') {
|
||||
return await Promise.resolve({
|
||||
group: [],
|
||||
credentials: [{ name: 'openAiApi' }],
|
||||
});
|
||||
}
|
||||
return await Promise.resolve({ group: [], credentials: [] });
|
||||
});
|
||||
(context.credentialService.list as jest.Mock).mockResolvedValue([]);
|
||||
|
||||
const result = await analyzeWorkflow(context, 'wf-1');
|
||||
|
||||
const sharedReq = result.find((r) => r.node.name === 'Shared Model');
|
||||
// Agent A executes first (left-most), so it claims the shared sub-node.
|
||||
expect(sharedReq?.subnodeRootNode?.name).toBe('Agent A');
|
||||
});
|
||||
|
||||
it('does not classify a sub-node by following a Main edge', async () => {
|
||||
const trigger = makeNode({
|
||||
name: 'Trigger',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
id: 'trigger-1',
|
||||
position: [0, 0] as [number, number],
|
||||
});
|
||||
const httpAction = makeNode({
|
||||
name: 'HTTP',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
id: 'http-1',
|
||||
position: [200, 0] as [number, number],
|
||||
});
|
||||
(context.workflowService.getAsWorkflowJSON as jest.Mock).mockResolvedValue(
|
||||
makeWorkflowJSON([trigger, httpAction], {
|
||||
Trigger: { main: [[{ node: 'HTTP', type: 'main', index: 0 }]] },
|
||||
}),
|
||||
);
|
||||
(context.nodeService.getDescription as jest.Mock).mockImplementation(async (type: string) => {
|
||||
if (type === 'n8n-nodes-base.webhook') {
|
||||
return await Promise.resolve({
|
||||
group: ['trigger'],
|
||||
credentials: [],
|
||||
webhooks: [{}],
|
||||
});
|
||||
}
|
||||
return await Promise.resolve({
|
||||
group: [],
|
||||
credentials: [{ name: 'httpBasicAuth' }],
|
||||
});
|
||||
});
|
||||
(context.credentialService.list as jest.Mock).mockResolvedValue([]);
|
||||
|
||||
const result = await analyzeWorkflow(context, 'wf-1');
|
||||
|
||||
const httpReq = result.find((r) => r.node.name === 'HTTP');
|
||||
expect(httpReq?.subnodeRootNode).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
import { findPlaceholderDetails } from '@n8n/utils';
|
||||
import type { IDataObject, NodeJSON, DisplayOptions, WorkflowJSON } from '@n8n/workflow-sdk';
|
||||
import { matchesDisplayOptions } from '@n8n/workflow-sdk';
|
||||
import type { IConnections, INode } from 'n8n-workflow';
|
||||
import { getParentNodes, mapConnectionsByDestination } from 'n8n-workflow';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import type { SetupRequest } from './setup-workflow.schema';
|
||||
|
|
@ -17,11 +19,12 @@ import type { InstanceAiContext } from '../../types';
|
|||
|
||||
/** Cache for deduplicating credential fetches across nodes with the same types. */
|
||||
export interface CredentialCache {
|
||||
/** Credential list promises, keyed by credential type. */
|
||||
/** Credential list promises, keyed by `${workflowId ?? ''}|${credentialType}` —
|
||||
* scope is part of the key so a cache shared across workflows stays correct. */
|
||||
lists: Map<string, Promise<Array<{ id: string; name: string }>>>;
|
||||
/** Testability check promises, keyed by credential type. */
|
||||
/** Testability check promises, keyed by credential type (workflow-independent). */
|
||||
testability: Map<string, Promise<boolean>>;
|
||||
/** Credential test result promises, keyed by credential ID. */
|
||||
/** Credential test result promises, keyed by credential ID (workflow-independent). */
|
||||
tests: Map<string, Promise<{ success: boolean; message?: string }>>;
|
||||
}
|
||||
|
||||
|
|
@ -29,6 +32,10 @@ export function createCredentialCache(): CredentialCache {
|
|||
return { lists: new Map(), testability: new Map(), tests: new Map() };
|
||||
}
|
||||
|
||||
function listCacheKey(workflowId: string | undefined, credentialType: string): string {
|
||||
return `${workflowId ?? ''}|${credentialType}`;
|
||||
}
|
||||
|
||||
// ── Node analysis ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
|
@ -158,6 +165,7 @@ export async function buildSetupRequests(
|
|||
node: NodeJSON,
|
||||
triggerTestResult?: { status: 'success' | 'error' | 'listening'; error?: string },
|
||||
cache?: CredentialCache,
|
||||
workflowId?: string,
|
||||
): Promise<SetupRequest[]> {
|
||||
if (!node.name) return [];
|
||||
if (node.disabled) return [];
|
||||
|
|
@ -292,13 +300,17 @@ export async function buildSetupRequests(
|
|||
: undefined;
|
||||
|
||||
if (credentialType) {
|
||||
// Use cache to avoid duplicate fetches for the same credential type across nodes
|
||||
let listPromise = cache?.lists.get(credentialType);
|
||||
// Use cache to avoid duplicate fetches for the same credential type across nodes.
|
||||
// Scope to the workflow so we list only credentials the save path will accept —
|
||||
// the editor's credential picker uses the same scoping. The cache key includes
|
||||
// workflowId so a cache shared across workflows stays correct.
|
||||
const cacheKey = listCacheKey(workflowId, credentialType);
|
||||
let listPromise = cache?.lists.get(cacheKey);
|
||||
if (!listPromise) {
|
||||
listPromise = context.credentialService
|
||||
.list({ type: credentialType })
|
||||
.list({ type: credentialType, ...(workflowId ? { workflowId } : {}) })
|
||||
.then((creds) => creds.map((c) => ({ id: c.id, name: c.name })));
|
||||
cache?.lists.set(credentialType, listPromise);
|
||||
cache?.lists.set(cacheKey, listPromise);
|
||||
}
|
||||
const sortedCreds = await listPromise;
|
||||
existingCredentials = sortedCreds.map((c) => ({ id: c.id, name: c.name }));
|
||||
|
|
@ -467,7 +479,7 @@ export function sortByExecutionOrder(
|
|||
if (visited.has(nodeName)) return;
|
||||
visited.add(nodeName);
|
||||
|
||||
// Visit AI sub-nodes BEFORE the parent (non-main incoming connections)
|
||||
// Visit AI sub-nodes before the node that consumes them (non-main incoming connections).
|
||||
const subNodes = nonMainIncoming.get(nodeName) ?? [];
|
||||
for (const subNode of subNodes) {
|
||||
dfs(subNode);
|
||||
|
|
@ -746,6 +758,77 @@ export function buildCompletedReport(
|
|||
return result;
|
||||
}
|
||||
|
||||
// ── Sub-node grouping ───────────────────────────────────────────────────────
|
||||
type SubnodeRootNode = Pick<INode, 'name' | 'type' | 'typeVersion' | 'id'>;
|
||||
|
||||
export function buildSubnodeToRootNodeMap(
|
||||
nodes: NodeJSON[],
|
||||
connections: IConnections,
|
||||
executionOrder: string[],
|
||||
): Map<string, SubnodeRootNode> {
|
||||
const connectionsByDestination = mapConnectionsByDestination(connections);
|
||||
|
||||
const directSubnodesByNodeName = new Map<string, string[]>();
|
||||
for (const node of nodes) {
|
||||
if (!node.name) continue;
|
||||
// Non-main upstream nodes are direct sub-nodes of this node.
|
||||
const subs = getParentNodes(connectionsByDestination, node.name, 'ALL_NON_MAIN', 1);
|
||||
if (subs.length > 0) directSubnodesByNodeName.set(node.name, subs);
|
||||
}
|
||||
if (directSubnodesByNodeName.size === 0) return new Map();
|
||||
|
||||
const allSubnodeNames = new Set<string>();
|
||||
for (const subs of directSubnodesByNodeName.values()) {
|
||||
for (const name of subs) allSubnodeNames.add(name);
|
||||
}
|
||||
const rootNodeNames = [...directSubnodesByNodeName.keys()].filter((n) => !allSubnodeNames.has(n));
|
||||
if (rootNodeNames.length === 0) return new Map();
|
||||
|
||||
const nodeByName = new Map<string, NodeJSON>();
|
||||
for (const node of nodes) {
|
||||
if (node.name) nodeByName.set(node.name, node);
|
||||
}
|
||||
|
||||
// Sort root nodes by execution order so the first to claim a sub-node
|
||||
// is the deterministic "owner" when multi-root ambiguity exists.
|
||||
const orderIndex = new Map<string, number>();
|
||||
for (let i = 0; i < executionOrder.length; i++) {
|
||||
orderIndex.set(executionOrder[i], i);
|
||||
}
|
||||
const sortedRootNodes = [...rootNodeNames].sort(
|
||||
(a, b) =>
|
||||
(orderIndex.get(a) ?? Number.MAX_SAFE_INTEGER) -
|
||||
(orderIndex.get(b) ?? Number.MAX_SAFE_INTEGER),
|
||||
);
|
||||
|
||||
const subnodeToRootNode = new Map<string, SubnodeRootNode>();
|
||||
|
||||
for (const rootNodeName of sortedRootNodes) {
|
||||
const rootNode = nodeByName.get(rootNodeName);
|
||||
if (!rootNode) continue;
|
||||
|
||||
const subnodeRootNode: SubnodeRootNode = {
|
||||
name: rootNodeName,
|
||||
type: rootNode.type,
|
||||
typeVersion: rootNode.typeVersion ?? 1,
|
||||
id: rootNode.id ?? '',
|
||||
};
|
||||
|
||||
const transitiveSubs = getParentNodes(
|
||||
connectionsByDestination,
|
||||
rootNodeName,
|
||||
'ALL_NON_MAIN',
|
||||
-1,
|
||||
);
|
||||
for (const subName of transitiveSubs) {
|
||||
if (subnodeToRootNode.has(subName)) continue;
|
||||
subnodeToRootNode.set(subName, subnodeRootNode);
|
||||
}
|
||||
}
|
||||
|
||||
return subnodeToRootNode;
|
||||
}
|
||||
|
||||
// ── Full workflow analysis ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
|
@ -762,7 +845,13 @@ export async function analyzeWorkflow(
|
|||
const cache = createCredentialCache();
|
||||
const allRequestArrays = await Promise.all(
|
||||
workflowJson.nodes.map(async (node) => {
|
||||
return await buildSetupRequests(context, node, triggerResults?.[node.name ?? ''], cache);
|
||||
return await buildSetupRequests(
|
||||
context,
|
||||
node,
|
||||
triggerResults?.[node.name ?? ''],
|
||||
cache,
|
||||
workflowId,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -785,5 +874,23 @@ export async function analyzeWorkflow(
|
|||
workflowJson.connections as unknown as Record<string, unknown>,
|
||||
);
|
||||
|
||||
// Stamp `subnodeRootNode` on every sub-node setup request so the frontend can
|
||||
// render the group header even when the root node has no setup request of
|
||||
// its own. Sub-node membership is derived from the full workflow graph,
|
||||
// not just the (filtered) setup requests.
|
||||
const subnodeToRootNode = buildSubnodeToRootNodeMap(
|
||||
workflowJson.nodes,
|
||||
workflowJson.connections as unknown as IConnections,
|
||||
setupRequests.map((req) => req.node.name),
|
||||
);
|
||||
if (subnodeToRootNode.size > 0) {
|
||||
for (const req of setupRequests) {
|
||||
const subnodeRootNode = subnodeToRootNode.get(req.node.name);
|
||||
if (subnodeRootNode) {
|
||||
req.subnodeRootNode = subnodeRootNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return setupRequests;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -251,7 +251,22 @@ export interface CredentialTypeSearchResult {
|
|||
}
|
||||
|
||||
export interface InstanceAiCredentialService {
|
||||
list(options?: { type?: string }): Promise<CredentialSummary[]>;
|
||||
/**
|
||||
* List credentials.
|
||||
*
|
||||
* Without `workflowId` / `projectId`: returns every credential the user has
|
||||
* read access to anywhere in the instance. Use this for informational lookups.
|
||||
*
|
||||
* With `workflowId` or `projectId`: returns only credentials usable in that
|
||||
* workflow / project (the same scoping the editor's credential picker uses).
|
||||
* Use this whenever the result feeds a setup card the user will pick from —
|
||||
* the save path enforces the same scope and will reject anything outside it.
|
||||
*/
|
||||
list(options?: {
|
||||
type?: string;
|
||||
workflowId?: string;
|
||||
projectId?: string;
|
||||
}): Promise<CredentialSummary[]>;
|
||||
get(credentialId: string): Promise<CredentialDetail>;
|
||||
delete(credentialId: string): Promise<void>;
|
||||
test(credentialId: string): Promise<{ success: boolean; message?: string }>;
|
||||
|
|
|
|||
|
|
@ -378,13 +378,59 @@ describe('cleanupTestExecutions — scope and deletion pipeline', () => {
|
|||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Credential adapter — IDOR boundary for confirmation payload credential IDs
|
||||
//
|
||||
// `setupWorkflowApply` and `credentialSelection` confirmation payloads carry
|
||||
// client-supplied credential IDs. The credential adapter resolves them
|
||||
// through `credentialsService.getOne(user, ...)`, which the underlying
|
||||
// service binds to the requesting user — IDs the user can't access throw
|
||||
// rather than leaking the credential.
|
||||
// Credential listing — workflow/project scoping
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('credentialService.list — scoping', () => {
|
||||
it('uses getCredentialsAUserCanUseInAWorkflow when workflowId is provided', async () => {
|
||||
credentialsService.getCredentialsAUserCanUseInAWorkflow.mockResolvedValue([
|
||||
{ id: 'c1', name: 'Slack Shared', type: 'slackApi' },
|
||||
{ id: 'c2', name: 'Notion', type: 'notionApi' },
|
||||
] as never);
|
||||
|
||||
const ctx = service.createContext(user);
|
||||
const result = await ctx.credentialService.list({ type: 'slackApi', workflowId: 'wf-1' });
|
||||
|
||||
expect(credentialsService.getCredentialsAUserCanUseInAWorkflow).toHaveBeenCalledWith(user, {
|
||||
workflowId: 'wf-1',
|
||||
});
|
||||
expect(credentialsService.getMany).not.toHaveBeenCalled();
|
||||
// type filter applied post-fetch
|
||||
expect(result).toEqual([{ id: 'c1', name: 'Slack Shared', type: 'slackApi' }]);
|
||||
});
|
||||
|
||||
it('uses getCredentialsAUserCanUseInAWorkflow when projectId is provided', async () => {
|
||||
credentialsService.getCredentialsAUserCanUseInAWorkflow.mockResolvedValue([
|
||||
{ id: 'c1', name: 'Slack Shared', type: 'slackApi' },
|
||||
] as never);
|
||||
|
||||
const ctx = service.createContext(user);
|
||||
await ctx.credentialService.list({ projectId: 'proj-1' });
|
||||
|
||||
expect(credentialsService.getCredentialsAUserCanUseInAWorkflow).toHaveBeenCalledWith(user, {
|
||||
projectId: 'proj-1',
|
||||
});
|
||||
expect(credentialsService.getMany).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('falls back to getMany (broad) when neither workflowId nor projectId is provided', async () => {
|
||||
credentialsService.getMany.mockResolvedValue([
|
||||
{ id: 'c1', name: 'Slack', type: 'slackApi' },
|
||||
] as never);
|
||||
|
||||
const ctx = service.createContext(user);
|
||||
await ctx.credentialService.list({ type: 'slackApi' });
|
||||
|
||||
expect(credentialsService.getMany).toHaveBeenCalledWith(user, {
|
||||
listQueryOptions: { filter: { type: 'slackApi' } },
|
||||
includeGlobal: true,
|
||||
});
|
||||
expect(credentialsService.getCredentialsAUserCanUseInAWorkflow).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Credential adapter — credential ownership revalidation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('credentialService.get — credential ownership revalidation', () => {
|
||||
|
|
|
|||
|
|
@ -125,6 +125,40 @@ import { Telemetry } from '@/telemetry';
|
|||
import { WorkflowRunner } from '@/workflow-runner';
|
||||
import { getBase } from '@/workflow-execute-additional-data';
|
||||
|
||||
/**
|
||||
* Fill in defaults for properties whose visibility depends on sibling values
|
||||
* (e.g. OpenAI v2's per-resource `operation`). A naive single-pass loop picks
|
||||
* the first variant of a duplicated property name, which leaves dependent
|
||||
* properties (like `modelId` for `text`/`response`) out of view of the issue
|
||||
* and credential checkers. `getNodeParameters` walks the dependency graph and
|
||||
* fills only displayed properties.
|
||||
*/
|
||||
function resolveDisplayedDefaults(
|
||||
nodeProperties: INodeProperties[],
|
||||
parameters: Record<string, unknown>,
|
||||
nodeType: string,
|
||||
typeVersion: number,
|
||||
desc: INodeTypeDescription,
|
||||
): INodeParameters {
|
||||
const stubNode: INode = {
|
||||
id: '',
|
||||
name: '',
|
||||
type: nodeType,
|
||||
typeVersion,
|
||||
parameters: parameters as INodeParameters,
|
||||
position: [0, 0],
|
||||
};
|
||||
const resolved = NodeHelpers.getNodeParameters(
|
||||
nodeProperties,
|
||||
parameters as INodeParameters,
|
||||
true,
|
||||
false,
|
||||
stubNode,
|
||||
desc,
|
||||
);
|
||||
return resolved ?? (parameters as INodeParameters);
|
||||
}
|
||||
|
||||
@Service()
|
||||
export class InstanceAiAdapterService {
|
||||
private readonly logger: Logger;
|
||||
|
|
@ -963,6 +997,30 @@ export class InstanceAiAdapterService {
|
|||
|
||||
return {
|
||||
async list(options) {
|
||||
// Setup flows scope to a workflow or project so the candidates match what
|
||||
// the save path will accept. `preventTampering` (workflow.service.ee.ts)
|
||||
// uses `getCredentialsAUserCanUseInAWorkflow` for the same intersection,
|
||||
// so the editor's credential picker and the AI's setup card stay aligned.
|
||||
if (options?.workflowId || options?.projectId) {
|
||||
const scoped = options.workflowId
|
||||
? await credentialsService.getCredentialsAUserCanUseInAWorkflow(user, {
|
||||
workflowId: options.workflowId,
|
||||
})
|
||||
: await credentialsService.getCredentialsAUserCanUseInAWorkflow(user, {
|
||||
projectId: options.projectId!,
|
||||
});
|
||||
|
||||
const filtered = options.type ? scoped.filter((c) => c.type === options.type) : scoped;
|
||||
|
||||
return filtered.map(
|
||||
(c): CredentialSummary => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
type: c.type,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const credentials = await credentialsService.getMany(user, {
|
||||
listQueryOptions: {
|
||||
filter: options?.type ? { type: options.type } : undefined,
|
||||
|
|
@ -1834,21 +1892,20 @@ export class InstanceAiAdapterService {
|
|||
if (!desc) return {};
|
||||
|
||||
const nodeProperties = desc.properties;
|
||||
|
||||
// Fill in default values for parameters not explicitly set
|
||||
const paramsWithDefaults: Record<string, unknown> = { ...parameters };
|
||||
for (const prop of nodeProperties) {
|
||||
if (!(prop.name in paramsWithDefaults) && prop.default !== undefined) {
|
||||
paramsWithDefaults[prop.name] = prop.default;
|
||||
}
|
||||
}
|
||||
const paramsWithDefaults = resolveDisplayedDefaults(
|
||||
nodeProperties,
|
||||
parameters,
|
||||
nodeType,
|
||||
typeVersion,
|
||||
desc as unknown as INodeTypeDescription,
|
||||
);
|
||||
|
||||
const minimalNode: INode = {
|
||||
id: '',
|
||||
name: '',
|
||||
type: nodeType,
|
||||
typeVersion,
|
||||
parameters: paramsWithDefaults as INodeParameters,
|
||||
parameters: paramsWithDefaults,
|
||||
position: [0, 0],
|
||||
};
|
||||
|
||||
|
|
@ -1880,7 +1937,7 @@ export class InstanceAiAdapterService {
|
|||
if (
|
||||
prop.displayOptions &&
|
||||
!NodeHelpers.displayParameter(
|
||||
paramsWithDefaults as INodeParameters,
|
||||
paramsWithDefaults,
|
||||
prop,
|
||||
minimalNode,
|
||||
desc as unknown as INodeTypeDescription,
|
||||
|
|
@ -1904,31 +1961,32 @@ export class InstanceAiAdapterService {
|
|||
|
||||
const credentialTypes = new Set<string>();
|
||||
|
||||
// 1. Displayable credentials from node type description
|
||||
const nodeCredentials = desc.credentials ?? [];
|
||||
// Fill defaults before evaluating display options
|
||||
const paramsWithDefaultsForCreds: Record<string, unknown> = { ...parameters };
|
||||
for (const prop of desc.properties) {
|
||||
if (!(prop.name in paramsWithDefaultsForCreds) && prop.default !== undefined) {
|
||||
paramsWithDefaultsForCreds[prop.name] = prop.default;
|
||||
}
|
||||
}
|
||||
const credCheckNode: INode = {
|
||||
const paramsWithDefaults = resolveDisplayedDefaults(
|
||||
desc.properties,
|
||||
parameters,
|
||||
nodeType,
|
||||
typeVersion,
|
||||
desc as unknown as INodeTypeDescription,
|
||||
);
|
||||
const minimalNode: INode = {
|
||||
id: '',
|
||||
name: '',
|
||||
type: nodeType,
|
||||
typeVersion,
|
||||
parameters: paramsWithDefaultsForCreds as INodeParameters,
|
||||
parameters: paramsWithDefaults,
|
||||
position: [0, 0],
|
||||
};
|
||||
|
||||
// 1. Displayable credentials from node type description
|
||||
const nodeCredentials = desc.credentials ?? [];
|
||||
for (const cred of nodeCredentials) {
|
||||
// Check if credential is displayable given current parameters
|
||||
if (cred.displayOptions) {
|
||||
if (
|
||||
!NodeHelpers.displayParameter(
|
||||
paramsWithDefaultsForCreds as INodeParameters,
|
||||
paramsWithDefaults,
|
||||
cred,
|
||||
credCheckNode,
|
||||
minimalNode,
|
||||
desc as unknown as INodeTypeDescription,
|
||||
)
|
||||
) {
|
||||
|
|
@ -1939,20 +1997,6 @@ export class InstanceAiAdapterService {
|
|||
}
|
||||
|
||||
// 2. Node issues for dynamic credentials (e.g. HTTP Request missing auth)
|
||||
const paramsWithDefaults: Record<string, unknown> = { ...parameters };
|
||||
for (const prop of desc.properties) {
|
||||
if (!(prop.name in paramsWithDefaults) && prop.default !== undefined) {
|
||||
paramsWithDefaults[prop.name] = prop.default;
|
||||
}
|
||||
}
|
||||
const minimalNode: INode = {
|
||||
id: '',
|
||||
name: '',
|
||||
type: nodeType,
|
||||
typeVersion,
|
||||
parameters: paramsWithDefaults as INodeParameters,
|
||||
position: [0, 0],
|
||||
};
|
||||
const issues = NodeHelpers.getNodeParametersIssues(
|
||||
desc.properties,
|
||||
minimalNode,
|
||||
|
|
|
|||
|
|
@ -5450,6 +5450,7 @@
|
|||
"instanceAi.workflowSetup.triggerError": "Trigger test failed",
|
||||
"instanceAi.workflowSetup.triggerListening": "Listening for events…",
|
||||
"instanceAi.workflowSetup.apply": "Apply",
|
||||
"instanceAi.workflowSetup.credentialTestFailedTooltip": "Credential test failed. Update the credential and try again.",
|
||||
"instanceAi.workflowSetup.applyCompleted": "Apply completed",
|
||||
"instanceAi.workflowSetup.later": "Skip setup for now",
|
||||
"instanceAi.workflowSetup.loading": "Loading setup…",
|
||||
|
|
@ -5457,10 +5458,14 @@
|
|||
"instanceAi.workflowSetup.applied": "Workflow configured",
|
||||
"instanceAi.workflowSetup.partiallyApplied": "Partially configured — some nodes skipped",
|
||||
"instanceAi.workflowSetup.deferred": "Skipped — configure later",
|
||||
"instanceAi.workflowSetup.cardSkipped": "Skipped",
|
||||
"instanceAi.workflowSetup.selectOption": "Select an option",
|
||||
"instanceAi.workflowSetup.confirmTitle": "Ready to apply",
|
||||
"instanceAi.workflowSetup.confirmDescription": "{count} item configured | {count} items configured",
|
||||
"instanceAi.workflowSetup.reviewDetails": "Review details",
|
||||
"instanceAi.workflowSetup.stepCounter": "{current} of {total}",
|
||||
"instanceAi.workflowSetup.prevStep": "Previous step",
|
||||
"instanceAi.workflowSetup.nextStep": "Next step",
|
||||
"instanceAi.debug.title": "Debug",
|
||||
"instanceAi.debug.copyTrace": "Copy trace",
|
||||
"instanceAi.debug.timing": "Timing",
|
||||
|
|
|
|||
|
|
@ -644,6 +644,7 @@ export interface NewCredentialsModal extends ModalState {
|
|||
projectId?: string;
|
||||
suggestedName?: string;
|
||||
nodeName?: string;
|
||||
contextNode?: INodeUi;
|
||||
hideAskAssistant?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ import type {
|
|||
ModalKey,
|
||||
AppliedThemeOption,
|
||||
TabOptions,
|
||||
INodeUi,
|
||||
} from '@/Interface';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
|
|
@ -541,6 +542,7 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
|||
modalsById.value[CREDENTIAL_EDIT_MODAL_KEY] = {
|
||||
...modalsById.value[CREDENTIAL_EDIT_MODAL_KEY],
|
||||
projectId: undefined,
|
||||
contextNode: undefined,
|
||||
hideAskAssistant: options.hideAskAssistant,
|
||||
} as NewCredentialsModal;
|
||||
openModal(CREDENTIAL_EDIT_MODAL_KEY);
|
||||
|
|
@ -553,6 +555,7 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
|||
projectId?: string,
|
||||
suggestedName?: string,
|
||||
nodeName?: string,
|
||||
contextNode?: INodeUi,
|
||||
options: { hideAskAssistant?: boolean } = {},
|
||||
) => {
|
||||
setActiveId(CREDENTIAL_EDIT_MODAL_KEY, type);
|
||||
|
|
@ -563,6 +566,7 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
|||
projectId,
|
||||
suggestedName,
|
||||
nodeName,
|
||||
contextNode,
|
||||
hideAskAssistant: options.hideAskAssistant,
|
||||
} as NewCredentialsModal;
|
||||
setMode(CREDENTIAL_EDIT_MODAL_KEY, 'new');
|
||||
|
|
|
|||
|
|
@ -186,6 +186,7 @@ describe('agent integration credential picker usage', () => {
|
|||
'project-1',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{ hideAskAssistant: true },
|
||||
);
|
||||
});
|
||||
|
|
@ -217,6 +218,7 @@ describe('agent integration credential picker usage', () => {
|
|||
'project-1',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{ hideAskAssistant: true },
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -380,6 +380,7 @@ function onCreateCredential(integration: ChatIntegrationDescriptor) {
|
|||
props.data.projectId,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
hideAskAssistant: true,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -316,6 +316,7 @@ function onCreateCredential(type: string) {
|
|||
props.projectId,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
hideAskAssistant: true,
|
||||
},
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,133 @@
|
|||
import { computed, ref } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import WorkflowSetupSectionBody from '../workflowSetup/components/WorkflowSetupSectionBody.vue';
|
||||
import { makeWorkflowSetupSection } from '../workflowSetup/__tests__/factories';
|
||||
import type { WorkflowSetupContext } from '../workflowSetup/composables/useWorkflowSetupContext';
|
||||
import type { WorkflowSetupSection } from '../workflowSetup/workflowSetup.types';
|
||||
|
||||
const workflowSetupContext = vi.hoisted(() => ({
|
||||
current: undefined as unknown as WorkflowSetupContext,
|
||||
}));
|
||||
|
||||
vi.mock('../workflowSetup/composables/useWorkflowSetupContext', () => ({
|
||||
useWorkflowSetupContext: () => workflowSetupContext.current,
|
||||
}));
|
||||
|
||||
vi.mock('@/features/credentials/components/NodeCredentials.vue', () => ({
|
||||
default: { template: '<div><slot name="label-postfix" /></div>' },
|
||||
}));
|
||||
|
||||
vi.mock('@/features/ndv/parameters/components/ParameterInputList.vue', () => ({
|
||||
default: { template: '<div />' },
|
||||
}));
|
||||
|
||||
vi.mock('@n8n/i18n', async (importOriginal) => ({
|
||||
...(await importOriginal()),
|
||||
useI18n: () => ({
|
||||
baseText: (key: string, options?: { interpolate?: Record<string, string | number> }) => {
|
||||
if (key === 'instanceAi.workflowSetup.usedByNodes') {
|
||||
return `Used by ${options?.interpolate?.count} nodes`;
|
||||
}
|
||||
return key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/features/credentials/credentials.store', () => ({
|
||||
useCredentialsStore: () => ({
|
||||
getCredentialTypeByName: () => ({ displayName: 'HTTP Basic Auth' }),
|
||||
getCredentialById: () => undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/app/stores/nodeTypes.store', () => ({
|
||||
useNodeTypesStore: () => ({
|
||||
getNodeType: () => null,
|
||||
communityNodeType: () => null,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/features/settings/environments.ee/environments.store', () => ({
|
||||
default: () => ({ variablesAsObject: {} }),
|
||||
}));
|
||||
|
||||
function makeContext(): WorkflowSetupContext {
|
||||
return {
|
||||
sections: computed(() => []),
|
||||
steps: computed(() => []),
|
||||
currentStepIndex: ref(0),
|
||||
activeStep: computed(() => undefined),
|
||||
hasOtherUnhandledSteps: computed(() => false),
|
||||
canAdvanceToNextIncomplete: computed(() => false),
|
||||
credentialSelections: ref({}),
|
||||
terminalState: ref(null),
|
||||
isReady: ref(true),
|
||||
projectId: computed(() => undefined),
|
||||
credentialFlow: computed(() => undefined),
|
||||
isActionPending: ref(false),
|
||||
setCredential: vi.fn(),
|
||||
setParameterValue: vi.fn(),
|
||||
getDisplayNode: (section) => section.node as INodeUi,
|
||||
isSectionComplete: () => false,
|
||||
isCredentialTestFailed: () => false,
|
||||
isSectionSkipped: () => false,
|
||||
isStepComplete: () => false,
|
||||
isStepSkipped: () => false,
|
||||
isStepHandled: () => false,
|
||||
goToStep: vi.fn(),
|
||||
goToNext: vi.fn(),
|
||||
goToPrev: vi.fn(),
|
||||
goToNextIncomplete: vi.fn(),
|
||||
apply: vi.fn(async () => {}),
|
||||
skipCurrentStep: vi.fn(async () => {}),
|
||||
};
|
||||
}
|
||||
|
||||
function renderComponent(section: WorkflowSetupSection) {
|
||||
workflowSetupContext.current = makeContext();
|
||||
|
||||
return mount(WorkflowSetupSectionBody, {
|
||||
props: { section },
|
||||
global: {
|
||||
stubs: {
|
||||
N8nText: { template: '<span><slot /></span>' },
|
||||
N8nTooltip: {
|
||||
template:
|
||||
'<div data-test-id="instance-ai-workflow-setup-card-tooltip"><span data-test-id="instance-ai-workflow-setup-card-tooltip-content"><slot name="content" /></span><slot /></div>',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe('WorkflowSetupSectionBody', () => {
|
||||
it('hides the grouped nodes hint for single-target sections', () => {
|
||||
const section = makeWorkflowSetupSection();
|
||||
|
||||
const wrapper = renderComponent(section);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-id="instance-ai-workflow-setup-card-nodes-hint"]').exists(),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('shows grouped nodes count and tooltip for multi-target sections', () => {
|
||||
const section = makeWorkflowSetupSection({
|
||||
credentialTargetNodes: [
|
||||
{ id: 'primary', name: 'Primary', type: 'n8n-nodes-base.httpRequest' },
|
||||
{ id: 'follower', name: 'Follower', type: 'n8n-nodes-base.httpRequest' },
|
||||
],
|
||||
});
|
||||
|
||||
const wrapper = renderComponent(section);
|
||||
|
||||
expect(wrapper.get('[data-test-id="instance-ai-workflow-setup-card-nodes-hint"]').text()).toBe(
|
||||
'Used by 2 nodes',
|
||||
);
|
||||
expect(
|
||||
wrapper.get('[data-test-id="instance-ai-workflow-setup-card-tooltip-content"]').text(),
|
||||
).toBe('Primary, Follower');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
import { computed, nextTick, ref, type Ref } from 'vue';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import WorkflowSetupWizard from '../workflowSetup/components/WorkflowSetupWizard.vue';
|
||||
import type { WorkflowSetupContext } from '../workflowSetup/composables/useWorkflowSetupContext';
|
||||
import { makeWorkflowSetupSection } from '../workflowSetup/__tests__/factories';
|
||||
import type { WorkflowSetupSection, WorkflowSetupStep } from '../workflowSetup/workflowSetup.types';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
|
||||
const workflowSetupContext = vi.hoisted(() => ({
|
||||
current: undefined as unknown as WorkflowSetupContext,
|
||||
}));
|
||||
|
||||
vi.mock('../workflowSetup/composables/useWorkflowSetupContext', () => ({
|
||||
useWorkflowSetupContext: () => workflowSetupContext.current,
|
||||
}));
|
||||
|
||||
vi.mock('@n8n/i18n', async (importOriginal) => ({
|
||||
...(await importOriginal()),
|
||||
useI18n: () => ({
|
||||
baseText: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
const renderComponent = createComponentRenderer(WorkflowSetupWizard, {
|
||||
global: {
|
||||
stubs: {
|
||||
ConfirmationFooter: {
|
||||
template: '<footer><slot /></footer>',
|
||||
},
|
||||
WorkflowSetupCard: {
|
||||
template:
|
||||
'<section data-test-id="instance-ai-workflow-setup-card"><slot name="footer" /></section>',
|
||||
},
|
||||
WorkflowSetupGroupCard: {
|
||||
template:
|
||||
'<section data-test-id="instance-ai-workflow-setup-group-card"><slot name="footer" /></section>',
|
||||
},
|
||||
N8nButton: {
|
||||
props: ['disabled', 'label'],
|
||||
emits: ['click'],
|
||||
template:
|
||||
'<button :disabled="disabled" @click="$emit(\'click\')">{{ label }}<slot /></button>',
|
||||
},
|
||||
N8nIconButton: {
|
||||
props: ['disabled', 'icon'],
|
||||
emits: ['click'],
|
||||
template: '<button :disabled="disabled" @click="$emit(\'click\')">{{ icon }}</button>',
|
||||
},
|
||||
N8nTooltip: {
|
||||
props: ['content', 'disabled'],
|
||||
template:
|
||||
'<div data-test-id="instance-ai-workflow-setup-tooltip" :data-content="content" :data-disabled="disabled"><slot /></div>',
|
||||
},
|
||||
N8nIcon: {
|
||||
template: '<span />',
|
||||
},
|
||||
N8nText: {
|
||||
template: '<span><slot /></span>',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const sectionA = makeWorkflowSetupSection();
|
||||
const sectionB = makeWorkflowSetupSection({
|
||||
id: 'Slack:slackApi',
|
||||
credentialType: 'slackApi',
|
||||
targetNodeName: 'Slack',
|
||||
node: {
|
||||
id: 'slack',
|
||||
type: 'n8n-nodes-base.slack',
|
||||
},
|
||||
});
|
||||
|
||||
interface ContextOptions {
|
||||
sections?: WorkflowSetupSection[];
|
||||
currentStepIndex?: Ref<number>;
|
||||
isSkipped?: Ref<boolean>;
|
||||
isCredentialTestFailed?: Ref<boolean>;
|
||||
isActionPending?: Ref<boolean>;
|
||||
hasOtherUnhandledSteps?: Ref<boolean>;
|
||||
}
|
||||
|
||||
function makeContext(isComplete: Ref<boolean>, options: ContextOptions = {}): WorkflowSetupContext {
|
||||
const sections = options.sections ?? [sectionA];
|
||||
const currentStepIndex = options.currentStepIndex ?? ref(0);
|
||||
const steps = computed<WorkflowSetupStep[]>(() =>
|
||||
sections.map((section) => ({ kind: 'section', section })),
|
||||
);
|
||||
|
||||
const isStepHandled = (step: WorkflowSetupStep): boolean => {
|
||||
if (step.kind !== 'section') return false;
|
||||
return isComplete.value || (options.isSkipped?.value ?? false);
|
||||
};
|
||||
|
||||
return {
|
||||
sections: computed(() => sections),
|
||||
steps,
|
||||
currentStepIndex,
|
||||
activeStep: computed(() => steps.value[currentStepIndex.value]),
|
||||
hasOtherUnhandledSteps: computed(() => options.hasOtherUnhandledSteps?.value ?? false),
|
||||
canAdvanceToNextIncomplete: computed(() => false),
|
||||
credentialSelections: ref({}),
|
||||
terminalState: ref(null),
|
||||
isReady: ref(true),
|
||||
projectId: computed(() => undefined),
|
||||
credentialFlow: computed(() => undefined),
|
||||
isActionPending: options.isActionPending ?? ref(false),
|
||||
setCredential: vi.fn(),
|
||||
setParameterValue: vi.fn(),
|
||||
getDisplayNode: (section) => section.node as INodeUi,
|
||||
isSectionComplete: () => isComplete.value,
|
||||
isCredentialTestFailed: () => options.isCredentialTestFailed?.value ?? false,
|
||||
isSectionSkipped: () => options.isSkipped?.value ?? false,
|
||||
isStepComplete: () => isComplete.value,
|
||||
isStepSkipped: () => options.isSkipped?.value ?? false,
|
||||
isStepHandled,
|
||||
goToStep: vi.fn(),
|
||||
goToNext: vi.fn(),
|
||||
goToPrev: vi.fn(),
|
||||
goToNextIncomplete: vi.fn(),
|
||||
apply: vi.fn(async () => {}),
|
||||
skipCurrentStep: vi.fn(async () => {}),
|
||||
};
|
||||
}
|
||||
|
||||
describe('WorkflowSetupWizard', () => {
|
||||
it('hides the skip action once the active setup step is complete', async () => {
|
||||
const isComplete = ref(false);
|
||||
workflowSetupContext.current = makeContext(isComplete);
|
||||
|
||||
const { queryByTestId } = renderComponent();
|
||||
|
||||
expect(queryByTestId('instance-ai-workflow-setup-later')).not.toBeNull();
|
||||
|
||||
isComplete.value = true;
|
||||
await nextTick();
|
||||
|
||||
expect(queryByTestId('instance-ai-workflow-setup-later')).toBeNull();
|
||||
expect(queryByTestId('instance-ai-workflow-setup-apply')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('hides footer navigation arrows when there is only one setup step', () => {
|
||||
workflowSetupContext.current = makeContext(ref(false));
|
||||
|
||||
const { queryByTestId } = renderComponent();
|
||||
|
||||
expect(queryByTestId('instance-ai-workflow-setup-prev')).toBeNull();
|
||||
expect(queryByTestId('instance-ai-workflow-setup-next')).toBeNull();
|
||||
});
|
||||
|
||||
it('disables footer navigation arrows at the first and last setup steps', async () => {
|
||||
const currentStepIndex = ref(0);
|
||||
workflowSetupContext.current = makeContext(ref(false), {
|
||||
sections: [sectionA, sectionB],
|
||||
currentStepIndex,
|
||||
});
|
||||
|
||||
const { getByTestId } = renderComponent();
|
||||
|
||||
expect(getByTestId('instance-ai-workflow-setup-prev')).toHaveAttribute('disabled');
|
||||
expect(getByTestId('instance-ai-workflow-setup-next')).not.toHaveAttribute('disabled');
|
||||
|
||||
currentStepIndex.value = 1;
|
||||
await nextTick();
|
||||
|
||||
expect(getByTestId('instance-ai-workflow-setup-prev')).not.toHaveAttribute('disabled');
|
||||
expect(getByTestId('instance-ai-workflow-setup-next')).toHaveAttribute('disabled');
|
||||
});
|
||||
|
||||
it('disables the skip action while a footer action is pending', async () => {
|
||||
const isActionPending = ref(true);
|
||||
workflowSetupContext.current = makeContext(ref(false), { isActionPending });
|
||||
|
||||
const { getByTestId } = renderComponent();
|
||||
|
||||
expect(getByTestId('instance-ai-workflow-setup-later')).toHaveAttribute('disabled');
|
||||
|
||||
isActionPending.value = false;
|
||||
await nextTick();
|
||||
|
||||
expect(getByTestId('instance-ai-workflow-setup-later')).not.toHaveAttribute('disabled');
|
||||
});
|
||||
|
||||
it('enables the primary footer action for skipped setup steps', async () => {
|
||||
const isSkipped = ref(false);
|
||||
workflowSetupContext.current = makeContext(ref(false), { isSkipped });
|
||||
|
||||
const { getByTestId, queryByTestId } = renderComponent();
|
||||
|
||||
expect(getByTestId('instance-ai-workflow-setup-apply')).toHaveAttribute('disabled');
|
||||
expect(queryByTestId('instance-ai-workflow-setup-later')).not.toBeNull();
|
||||
|
||||
isSkipped.value = true;
|
||||
await nextTick();
|
||||
|
||||
expect(getByTestId('instance-ai-workflow-setup-apply')).not.toHaveAttribute('disabled');
|
||||
expect(queryByTestId('instance-ai-workflow-setup-later')).toBeNull();
|
||||
});
|
||||
|
||||
it('shows the continue footer action instead of apply when there are other unhandled steps', () => {
|
||||
workflowSetupContext.current = makeContext(ref(true), { hasOtherUnhandledSteps: ref(true) });
|
||||
|
||||
const { getByTestId, queryByTestId } = renderComponent();
|
||||
|
||||
expect(getByTestId('instance-ai-workflow-setup-continue')).not.toHaveAttribute('disabled');
|
||||
expect(queryByTestId('instance-ai-workflow-setup-apply')).toBeNull();
|
||||
});
|
||||
|
||||
it('enables the footer tooltip when the active credential test failed', () => {
|
||||
workflowSetupContext.current = makeContext(ref(true), { isCredentialTestFailed: ref(true) });
|
||||
|
||||
const { getByTestId } = renderComponent();
|
||||
|
||||
expect(getByTestId('instance-ai-workflow-setup-tooltip')).toHaveAttribute(
|
||||
'data-content',
|
||||
'instanceAi.workflowSetup.credentialTestFailedTooltip',
|
||||
);
|
||||
expect(getByTestId('instance-ai-workflow-setup-tooltip')).toHaveAttribute(
|
||||
'data-disabled',
|
||||
'false',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,392 +0,0 @@
|
|||
import { describe, test, expect } from 'vitest';
|
||||
import type { InstanceAiWorkflowSetupNode } from '@n8n/api-types';
|
||||
import {
|
||||
credGroupKey,
|
||||
isParamValueSet,
|
||||
isNestedParam,
|
||||
toNodeUi,
|
||||
isTriggerOnly,
|
||||
shouldUseCredentialIcon,
|
||||
type SetupCard,
|
||||
} from '../instanceAiWorkflowSetup.utils';
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function makeSetupNode(
|
||||
overrides: Partial<InstanceAiWorkflowSetupNode> = {},
|
||||
): InstanceAiWorkflowSetupNode {
|
||||
return {
|
||||
node: {
|
||||
name: 'Slack',
|
||||
type: 'n8n-nodes-base.slack',
|
||||
typeVersion: 2,
|
||||
parameters: {},
|
||||
position: [250, 300] as [number, number],
|
||||
id: 'node-1',
|
||||
},
|
||||
isTrigger: false,
|
||||
...overrides,
|
||||
} as InstanceAiWorkflowSetupNode;
|
||||
}
|
||||
|
||||
function makeCard(overrides: Partial<SetupCard> = {}): SetupCard {
|
||||
return {
|
||||
id: 'card-1',
|
||||
nodes: [makeSetupNode()],
|
||||
isTrigger: false,
|
||||
isFirstTrigger: false,
|
||||
isTestable: false,
|
||||
isAutoApplied: false,
|
||||
hasParamIssues: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// credGroupKey
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('credGroupKey', () => {
|
||||
test('returns credential type as key for standard nodes', () => {
|
||||
const req = makeSetupNode({ credentialType: 'slackApi' });
|
||||
expect(credGroupKey(req)).toBe('slackApi');
|
||||
});
|
||||
|
||||
test('returns node name when credentialType is missing', () => {
|
||||
const req = makeSetupNode({ credentialType: undefined });
|
||||
expect(credGroupKey(req)).toBe('Slack');
|
||||
});
|
||||
|
||||
test('includes URL for HTTP Request nodes', () => {
|
||||
const req = makeSetupNode({
|
||||
credentialType: 'httpBasicAuth',
|
||||
node: {
|
||||
name: 'HTTP Request',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4,
|
||||
parameters: { url: 'https://api.example.com' },
|
||||
position: [0, 0] as [number, number],
|
||||
id: 'node-http',
|
||||
},
|
||||
});
|
||||
expect(credGroupKey(req)).toBe('httpBasicAuth:http:https://api.example.com');
|
||||
});
|
||||
|
||||
test('uses expression-specific key for HTTP Request with expression URL', () => {
|
||||
const req = makeSetupNode({
|
||||
credentialType: 'httpBasicAuth',
|
||||
node: {
|
||||
name: 'HTTP Request',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4,
|
||||
parameters: { url: '={{ $json.url }}' },
|
||||
position: [0, 0] as [number, number],
|
||||
id: 'node-http',
|
||||
},
|
||||
});
|
||||
expect(credGroupKey(req)).toBe('httpBasicAuth:http:expr:HTTP Request');
|
||||
});
|
||||
|
||||
test('handles HTTP Request Tool node type', () => {
|
||||
const req = makeSetupNode({
|
||||
credentialType: 'httpBasicAuth',
|
||||
node: {
|
||||
name: 'HTTP Tool',
|
||||
type: 'n8n-nodes-base.httpRequestTool',
|
||||
typeVersion: 1,
|
||||
parameters: { url: 'https://api.test.com' },
|
||||
position: [0, 0] as [number, number],
|
||||
id: 'node-http-tool',
|
||||
},
|
||||
});
|
||||
expect(credGroupKey(req)).toContain('httpBasicAuth:http:');
|
||||
});
|
||||
|
||||
test('groups two non-HTTP nodes sharing a credential type under one key', () => {
|
||||
const slackA = makeSetupNode({
|
||||
credentialType: 'slackApi',
|
||||
node: {
|
||||
name: 'Slack A',
|
||||
type: 'n8n-nodes-base.slack',
|
||||
typeVersion: 2,
|
||||
parameters: {},
|
||||
position: [0, 0] as [number, number],
|
||||
id: 'a',
|
||||
},
|
||||
});
|
||||
const slackB = makeSetupNode({
|
||||
credentialType: 'slackApi',
|
||||
node: {
|
||||
name: 'Slack B',
|
||||
type: 'n8n-nodes-base.slack',
|
||||
typeVersion: 2,
|
||||
parameters: {},
|
||||
position: [0, 0] as [number, number],
|
||||
id: 'b',
|
||||
},
|
||||
});
|
||||
expect(credGroupKey(slackA)).toBe(credGroupKey(slackB));
|
||||
});
|
||||
|
||||
test('splits HTTP Request nodes with different static URLs even for same credential type', () => {
|
||||
const httpA = makeSetupNode({
|
||||
credentialType: 'httpBasicAuth',
|
||||
node: {
|
||||
name: 'HTTP A',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4,
|
||||
parameters: { url: 'https://a.example.com' },
|
||||
position: [0, 0] as [number, number],
|
||||
id: 'a',
|
||||
},
|
||||
});
|
||||
const httpB = makeSetupNode({
|
||||
credentialType: 'httpBasicAuth',
|
||||
node: {
|
||||
name: 'HTTP B',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4,
|
||||
parameters: { url: 'https://b.example.com' },
|
||||
position: [0, 0] as [number, number],
|
||||
id: 'b',
|
||||
},
|
||||
});
|
||||
expect(credGroupKey(httpA)).not.toBe(credGroupKey(httpB));
|
||||
});
|
||||
|
||||
test('groups HTTP Request nodes with the same static URL and credential type', () => {
|
||||
const httpA = makeSetupNode({
|
||||
credentialType: 'httpBasicAuth',
|
||||
node: {
|
||||
name: 'HTTP A',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4,
|
||||
parameters: { url: 'https://api.example.com' },
|
||||
position: [0, 0] as [number, number],
|
||||
id: 'a',
|
||||
},
|
||||
});
|
||||
const httpB = makeSetupNode({
|
||||
credentialType: 'httpBasicAuth',
|
||||
node: {
|
||||
name: 'HTTP B',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4,
|
||||
parameters: { url: 'https://api.example.com' },
|
||||
position: [0, 0] as [number, number],
|
||||
id: 'b',
|
||||
},
|
||||
});
|
||||
expect(credGroupKey(httpA)).toBe(credGroupKey(httpB));
|
||||
});
|
||||
|
||||
test('splits HTTP Request nodes with expression URLs regardless of string equality', () => {
|
||||
const exprA = makeSetupNode({
|
||||
credentialType: 'httpBasicAuth',
|
||||
node: {
|
||||
name: 'HTTP A',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4,
|
||||
parameters: { url: '={{ $json.url }}' },
|
||||
position: [0, 0] as [number, number],
|
||||
id: 'a',
|
||||
},
|
||||
});
|
||||
const exprB = makeSetupNode({
|
||||
credentialType: 'httpBasicAuth',
|
||||
node: {
|
||||
name: 'HTTP B',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4,
|
||||
parameters: { url: '={{ $json.url }}' },
|
||||
position: [0, 0] as [number, number],
|
||||
id: 'b',
|
||||
},
|
||||
});
|
||||
expect(credGroupKey(exprA)).not.toBe(credGroupKey(exprB));
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// isParamValueSet
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('isParamValueSet', () => {
|
||||
test('returns false for undefined', () => {
|
||||
expect(isParamValueSet(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false for null', () => {
|
||||
expect(isParamValueSet(null)).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false for empty string', () => {
|
||||
expect(isParamValueSet('')).toBe(false);
|
||||
});
|
||||
|
||||
test('returns true for non-empty string', () => {
|
||||
expect(isParamValueSet('hello')).toBe(true);
|
||||
});
|
||||
|
||||
test('returns true for number', () => {
|
||||
expect(isParamValueSet(42)).toBe(true);
|
||||
});
|
||||
|
||||
test('returns true for boolean', () => {
|
||||
expect(isParamValueSet(false)).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false for empty resource locator', () => {
|
||||
expect(isParamValueSet({ __rl: true, value: '', mode: 'list' })).toBe(false);
|
||||
});
|
||||
|
||||
test('returns true for non-empty resource locator', () => {
|
||||
expect(isParamValueSet({ __rl: true, value: 'some-id', mode: 'list' })).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false for placeholder sentinel string', () => {
|
||||
expect(isParamValueSet('<__PLACEHOLDER_VALUE__your_email__>')).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false for resource locator with placeholder sentinel value', () => {
|
||||
expect(
|
||||
isParamValueSet({
|
||||
__rl: true,
|
||||
value: '<__PLACEHOLDER_VALUE__channel_name__>',
|
||||
mode: 'list',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// isNestedParam
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('isNestedParam', () => {
|
||||
test('returns true for collection type', () => {
|
||||
expect(isNestedParam({ type: 'collection' } as INodeProperties)).toBe(true);
|
||||
});
|
||||
|
||||
test('returns true for fixedCollection type', () => {
|
||||
expect(isNestedParam({ type: 'fixedCollection' } as INodeProperties)).toBe(true);
|
||||
});
|
||||
|
||||
test('returns true for multipleValues typeOption', () => {
|
||||
expect(
|
||||
isNestedParam({
|
||||
type: 'string',
|
||||
typeOptions: { multipleValues: true },
|
||||
} as INodeProperties),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false for simple string type', () => {
|
||||
expect(isNestedParam({ type: 'string' } as INodeProperties)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// toNodeUi
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('toNodeUi', () => {
|
||||
test('maps setup node fields to INodeUi', () => {
|
||||
const req = makeSetupNode({
|
||||
node: {
|
||||
id: 'abc',
|
||||
name: 'Test',
|
||||
type: 'n8n-nodes-base.test',
|
||||
typeVersion: 3,
|
||||
position: [100, 200] as [number, number],
|
||||
parameters: { foo: 'bar' },
|
||||
},
|
||||
});
|
||||
const result = toNodeUi(req);
|
||||
expect(result.id).toBe('abc');
|
||||
expect(result.name).toBe('Test');
|
||||
expect(result.type).toBe('n8n-nodes-base.test');
|
||||
expect(result.typeVersion).toBe(3);
|
||||
expect(result.position).toEqual([100, 200]);
|
||||
expect(result.parameters).toEqual({ foo: 'bar' });
|
||||
});
|
||||
|
||||
test('includes credentials when defined', () => {
|
||||
const req = makeSetupNode({
|
||||
node: {
|
||||
id: 'abc',
|
||||
name: 'Test',
|
||||
type: 'n8n-nodes-base.test',
|
||||
typeVersion: 1,
|
||||
position: [0, 0] as [number, number],
|
||||
parameters: {},
|
||||
credentials: { slackApi: { id: 'cred-1', name: 'My Slack' } },
|
||||
},
|
||||
});
|
||||
const result = toNodeUi(req);
|
||||
expect(result.credentials).toEqual({ slackApi: { id: 'cred-1', name: 'My Slack' } });
|
||||
});
|
||||
|
||||
test('omits credentials when undefined', () => {
|
||||
const req = makeSetupNode({
|
||||
node: {
|
||||
id: 'abc',
|
||||
name: 'Test',
|
||||
type: 'n8n-nodes-base.test',
|
||||
typeVersion: 1,
|
||||
position: [0, 0] as [number, number],
|
||||
parameters: {},
|
||||
credentials: undefined,
|
||||
},
|
||||
});
|
||||
const result = toNodeUi(req);
|
||||
expect(result.credentials).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// isTriggerOnly / shouldUseCredentialIcon
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('isTriggerOnly', () => {
|
||||
const noParamWork = () => false;
|
||||
const hasParamWork = () => true;
|
||||
|
||||
test('returns true for trigger without credential or param work', () => {
|
||||
const card = makeCard({ isTrigger: true, credentialType: undefined });
|
||||
expect(isTriggerOnly(card, noParamWork)).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false if card has credential type', () => {
|
||||
const card = makeCard({ isTrigger: true, credentialType: 'slackApi' });
|
||||
expect(isTriggerOnly(card, noParamWork)).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false if card has param work', () => {
|
||||
const card = makeCard({ isTrigger: true });
|
||||
expect(isTriggerOnly(card, hasParamWork)).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false if not a trigger', () => {
|
||||
const card = makeCard({ isTrigger: false });
|
||||
expect(isTriggerOnly(card, noParamWork)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldUseCredentialIcon', () => {
|
||||
test('returns true for multi-node credential-grouping cards', () => {
|
||||
const card = makeCard({
|
||||
credentialType: 'googleOAuth2Api',
|
||||
nodes: [makeSetupNode(), makeSetupNode(), makeSetupNode()],
|
||||
});
|
||||
expect(shouldUseCredentialIcon(card)).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false for single-node cards', () => {
|
||||
const card = makeCard({ credentialType: 'slackApi', nodes: [makeSetupNode()] });
|
||||
expect(shouldUseCredentialIcon(card)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,338 +0,0 @@
|
|||
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { computed } from 'vue';
|
||||
import type { InstanceAiWorkflowSetupNode } from '@n8n/api-types';
|
||||
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import { useCredentialsStore } from '@/features/credentials/credentials.store';
|
||||
import { useCredentialGroupSelection } from '../composables/useCredentialGroupSelection';
|
||||
import type { SetupCard } from '../instanceAiWorkflowSetup.utils';
|
||||
import {
|
||||
createWorkflowDocumentId,
|
||||
useWorkflowDocumentStore,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
|
||||
vi.mock('@/app/stores/workflows.store', () => ({
|
||||
useWorkflowsStore: vi.fn(() => ({ workflowId: '' })),
|
||||
}));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function makeSetupNode(
|
||||
overrides: Partial<InstanceAiWorkflowSetupNode> = {},
|
||||
): InstanceAiWorkflowSetupNode {
|
||||
return {
|
||||
node: {
|
||||
name: 'Slack',
|
||||
type: 'n8n-nodes-base.slack',
|
||||
typeVersion: 2,
|
||||
parameters: {},
|
||||
position: [0, 0] as [number, number],
|
||||
id: 'node-1',
|
||||
},
|
||||
isTrigger: false,
|
||||
...overrides,
|
||||
} as InstanceAiWorkflowSetupNode;
|
||||
}
|
||||
|
||||
function makeCard(overrides: Partial<SetupCard> = {}): SetupCard {
|
||||
return {
|
||||
id: 'card-1',
|
||||
credentialType: 'slackApi',
|
||||
nodes: [makeSetupNode()],
|
||||
isTrigger: false,
|
||||
isFirstTrigger: false,
|
||||
isTestable: false,
|
||||
isAutoApplied: false,
|
||||
hasParamIssues: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('useCredentialGroupSelection', () => {
|
||||
let workflowDocumentStore: ReturnType<typeof useWorkflowDocumentStore>;
|
||||
let credentialsStore: ReturnType<typeof useCredentialsStore>;
|
||||
let nodeTypesStore: ReturnType<typeof useNodeTypesStore>;
|
||||
let uiStore: ReturnType<typeof useUIStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
const pinia = createTestingPinia({ stubActions: false });
|
||||
setActivePinia(pinia);
|
||||
workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(''));
|
||||
credentialsStore = useCredentialsStore();
|
||||
nodeTypesStore = useNodeTypesStore();
|
||||
uiStore = useUIStore();
|
||||
});
|
||||
|
||||
describe('initCredGroupSelections', () => {
|
||||
test('picks up existing credential from node', () => {
|
||||
const card = makeCard({
|
||||
nodes: [
|
||||
makeSetupNode({
|
||||
credentialType: 'slackApi',
|
||||
node: {
|
||||
name: 'Slack',
|
||||
type: 'n8n-nodes-base.slack',
|
||||
typeVersion: 2,
|
||||
parameters: {},
|
||||
position: [0, 0] as [number, number],
|
||||
id: 'node-1',
|
||||
credentials: { slackApi: { id: 'cred-1', name: 'My Slack' } },
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const cards = computed(() => [card]);
|
||||
const { initCredGroupSelections, getCardCredentialId } = useCredentialGroupSelection(
|
||||
cards,
|
||||
vi.fn(),
|
||||
);
|
||||
initCredGroupSelections();
|
||||
|
||||
expect(getCardCredentialId(card)).toBe('cred-1');
|
||||
});
|
||||
|
||||
test('auto-selects when single existing credential is available', () => {
|
||||
const card = makeCard({
|
||||
nodes: [
|
||||
makeSetupNode({
|
||||
credentialType: 'slackApi',
|
||||
existingCredentials: [{ id: 'cred-1', name: 'My Slack' }],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const cards = computed(() => [card]);
|
||||
const { initCredGroupSelections, getCardCredentialId } = useCredentialGroupSelection(
|
||||
cards,
|
||||
vi.fn(),
|
||||
);
|
||||
initCredGroupSelections();
|
||||
|
||||
expect(getCardCredentialId(card)).toBe('cred-1');
|
||||
});
|
||||
|
||||
test('auto-selects first credential when auto-applied', () => {
|
||||
const card = makeCard({
|
||||
isAutoApplied: true,
|
||||
nodes: [
|
||||
makeSetupNode({
|
||||
credentialType: 'slackApi',
|
||||
existingCredentials: [
|
||||
{ id: 'cred-1', name: 'Slack 1' },
|
||||
{ id: 'cred-2', name: 'Slack 2' },
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const cards = computed(() => [card]);
|
||||
const { initCredGroupSelections, getCardCredentialId } = useCredentialGroupSelection(
|
||||
cards,
|
||||
vi.fn(),
|
||||
);
|
||||
initCredGroupSelections();
|
||||
|
||||
expect(getCardCredentialId(card)).toBe('cred-1');
|
||||
});
|
||||
|
||||
test('returns null when no credential is available', () => {
|
||||
const card = makeCard({
|
||||
nodes: [makeSetupNode({ credentialType: 'slackApi' })],
|
||||
});
|
||||
|
||||
const cards = computed(() => [card]);
|
||||
const { initCredGroupSelections, getCardCredentialId } = useCredentialGroupSelection(
|
||||
cards,
|
||||
vi.fn(),
|
||||
);
|
||||
initCredGroupSelections();
|
||||
|
||||
expect(getCardCredentialId(card)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setCredentialForGroup', () => {
|
||||
test('updates shared group state and syncs to store nodes', () => {
|
||||
const card = makeCard({
|
||||
nodes: [makeSetupNode({ credentialType: 'slackApi' })],
|
||||
});
|
||||
const storeNode = { name: 'Slack', credentials: {} };
|
||||
workflowDocumentStore.getNodeByName = vi.fn().mockReturnValue(storeNode);
|
||||
const mockGetById = (id: string) =>
|
||||
id === 'cred-2' ? { id: 'cred-2', name: 'New Slack' } : undefined;
|
||||
// @ts-expect-error Known pinia issue when spying on store getters
|
||||
vi.spyOn(credentialsStore, 'getCredentialById', 'get').mockReturnValue(mockGetById);
|
||||
|
||||
const testFn = vi.fn();
|
||||
const cards = computed(() => [card]);
|
||||
const { initCredGroupSelections, setCredentialForGroup, getCardCredentialId } =
|
||||
useCredentialGroupSelection(cards, testFn);
|
||||
initCredGroupSelections();
|
||||
|
||||
setCredentialForGroup('slackApi', 'slackApi', 'cred-2');
|
||||
|
||||
expect(getCardCredentialId(card)).toBe('cred-2');
|
||||
expect(storeNode.credentials).toEqual({
|
||||
slackApi: { id: 'cred-2', name: 'New Slack' },
|
||||
});
|
||||
});
|
||||
|
||||
test('triggers background credential test', () => {
|
||||
const card = makeCard({
|
||||
nodes: [makeSetupNode({ credentialType: 'slackApi' })],
|
||||
});
|
||||
workflowDocumentStore.getNodeByName = vi.fn().mockReturnValue(undefined);
|
||||
const mockGetById = () => ({ id: 'cred-2', name: 'New Slack' });
|
||||
// @ts-expect-error Known pinia issue when spying on store getters
|
||||
vi.spyOn(credentialsStore, 'getCredentialById', 'get').mockReturnValue(mockGetById);
|
||||
|
||||
const testFn = vi.fn().mockResolvedValue(undefined);
|
||||
const cards = computed(() => [card]);
|
||||
const { initCredGroupSelections, setCredentialForGroup } = useCredentialGroupSelection(
|
||||
cards,
|
||||
testFn,
|
||||
);
|
||||
initCredGroupSelections();
|
||||
|
||||
setCredentialForGroup('slackApi', 'slackApi', 'cred-2');
|
||||
|
||||
expect(testFn).toHaveBeenCalledWith('cred-2', 'New Slack', 'slackApi');
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearCredentialForGroup', () => {
|
||||
test('clears group state and removes credential from store nodes', () => {
|
||||
const card = makeCard({
|
||||
nodes: [makeSetupNode({ credentialType: 'slackApi' })],
|
||||
});
|
||||
const storeNode = {
|
||||
name: 'Slack',
|
||||
credentials: { slackApi: { id: 'cred-1', name: 'Old' } },
|
||||
};
|
||||
workflowDocumentStore.getNodeByName = vi.fn().mockReturnValue(storeNode);
|
||||
|
||||
const cards = computed(() => [card]);
|
||||
const { initCredGroupSelections, clearCredentialForGroup, getCardCredentialId } =
|
||||
useCredentialGroupSelection(cards, vi.fn());
|
||||
initCredGroupSelections();
|
||||
|
||||
clearCredentialForGroup('slackApi', 'slackApi');
|
||||
|
||||
expect(getCardCredentialId(card)).toBeNull();
|
||||
expect(storeNode.credentials).not.toHaveProperty('slackApi');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isFirstCardInCredGroup', () => {
|
||||
test('returns true for the first card in a group', () => {
|
||||
const card1 = makeCard({ id: 'card-1', credentialType: 'slackApi' });
|
||||
const card2 = makeCard({ id: 'card-2', credentialType: 'slackApi' });
|
||||
const cards = computed(() => [card1, card2]);
|
||||
|
||||
const { isFirstCardInCredGroup } = useCredentialGroupSelection(cards, vi.fn());
|
||||
|
||||
expect(isFirstCardInCredGroup(card1)).toBe(true);
|
||||
expect(isFirstCardInCredGroup(card2)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('openNewCredentialForSection', () => {
|
||||
function mockNodeTypeWithAuth(authOptions: Array<{ name: string; value: string }>) {
|
||||
const nodeType = {
|
||||
name: 'n8n-nodes-base.slack',
|
||||
properties: [
|
||||
{
|
||||
name: 'authentication',
|
||||
displayName: 'Authentication',
|
||||
type: 'options',
|
||||
default: authOptions[0]?.value ?? '',
|
||||
options: authOptions,
|
||||
},
|
||||
],
|
||||
credentials: [
|
||||
{
|
||||
name: 'slackApi',
|
||||
displayOptions: {
|
||||
show: { authentication: [authOptions[0]?.value ?? ''] },
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as INodeTypeDescription;
|
||||
// @ts-expect-error Known pinia issue when spying on store getters
|
||||
vi.spyOn(nodeTypesStore, 'getNodeType', 'get').mockReturnValue(() => nodeType);
|
||||
}
|
||||
|
||||
test('passes showAuthOptions=true and the node name when node has multiple auth options', () => {
|
||||
mockNodeTypeWithAuth([
|
||||
{ name: 'API Token', value: 'apiToken' },
|
||||
{ name: 'OAuth2', value: 'oAuth2' },
|
||||
]);
|
||||
|
||||
const card = makeCard({
|
||||
nodes: [makeSetupNode({ credentialType: 'slackApi' })],
|
||||
});
|
||||
const cards = computed(() => [card]);
|
||||
const { openNewCredentialForSection } = useCredentialGroupSelection(cards, vi.fn());
|
||||
const openSpy = vi.spyOn(uiStore, 'openNewCredential');
|
||||
|
||||
openNewCredentialForSection('slackApi', 'slackApi');
|
||||
|
||||
expect(openSpy).toHaveBeenCalledWith('slackApi', true, false, undefined, undefined, 'Slack');
|
||||
});
|
||||
|
||||
test('passes showAuthOptions=false when node has no main auth field', () => {
|
||||
// @ts-expect-error Known pinia issue when spying on store getters
|
||||
vi.spyOn(nodeTypesStore, 'getNodeType', 'get').mockReturnValue(() => null);
|
||||
|
||||
const card = makeCard({
|
||||
nodes: [makeSetupNode({ credentialType: 'slackApi' })],
|
||||
});
|
||||
const cards = computed(() => [card]);
|
||||
const { openNewCredentialForSection } = useCredentialGroupSelection(cards, vi.fn());
|
||||
const openSpy = vi.spyOn(uiStore, 'openNewCredential');
|
||||
|
||||
openNewCredentialForSection('slackApi', 'slackApi');
|
||||
|
||||
expect(openSpy).toHaveBeenCalledWith('slackApi', false, false, undefined, undefined, 'Slack');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cardHasExistingCredentials', () => {
|
||||
test('returns true when node has existing credentials', () => {
|
||||
const card = makeCard({
|
||||
nodes: [
|
||||
makeSetupNode({
|
||||
credentialType: 'slackApi',
|
||||
existingCredentials: [{ id: 'cred-1', name: 'My Slack' }],
|
||||
}),
|
||||
],
|
||||
});
|
||||
// @ts-expect-error Known pinia issue when spying on store getters
|
||||
vi.spyOn(credentialsStore, 'getUsableCredentialByType', 'get').mockReturnValue(() => []);
|
||||
|
||||
const cards = computed(() => [card]);
|
||||
const { cardHasExistingCredentials } = useCredentialGroupSelection(cards, vi.fn());
|
||||
|
||||
expect(cardHasExistingCredentials(card)).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false when no credentials exist', () => {
|
||||
const card = makeCard({
|
||||
nodes: [makeSetupNode({ credentialType: 'slackApi' })],
|
||||
});
|
||||
// @ts-expect-error Known pinia issue when spying on store getters
|
||||
vi.spyOn(credentialsStore, 'getUsableCredentialByType', 'get').mockReturnValue(() => []);
|
||||
|
||||
const cards = computed(() => [card]);
|
||||
const { cardHasExistingCredentials } = useCredentialGroupSelection(cards, vi.fn());
|
||||
|
||||
expect(cardHasExistingCredentials(card)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,197 +0,0 @@
|
|||
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { useCredentialsStore } from '@/features/credentials/credentials.store';
|
||||
import { useCredentialTesting } from '../composables/useCredentialTesting';
|
||||
import type { SetupCard } from '../instanceAiWorkflowSetup.utils';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function makeCard(overrides: Partial<SetupCard> = {}): SetupCard {
|
||||
return {
|
||||
id: 'card-1',
|
||||
credentialType: 'slackApi',
|
||||
nodes: [
|
||||
{
|
||||
node: {
|
||||
name: 'Slack',
|
||||
type: 'n8n-nodes-base.slack',
|
||||
typeVersion: 2,
|
||||
parameters: {},
|
||||
position: [0, 0] as [number, number],
|
||||
id: 'node-1',
|
||||
credentials: { slackApi: { id: 'cred-1', name: 'My Slack' } },
|
||||
},
|
||||
isTrigger: false,
|
||||
},
|
||||
],
|
||||
isTrigger: false,
|
||||
isFirstTrigger: false,
|
||||
isTestable: false,
|
||||
isAutoApplied: false,
|
||||
hasParamIssues: false,
|
||||
...overrides,
|
||||
} as SetupCard;
|
||||
}
|
||||
|
||||
describe('useCredentialTesting', () => {
|
||||
let credentialsStore: ReturnType<typeof useCredentialsStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
const pinia = createTestingPinia({ stubActions: false });
|
||||
setActivePinia(pinia);
|
||||
credentialsStore = useCredentialsStore();
|
||||
});
|
||||
|
||||
describe('testCredentialInBackground', () => {
|
||||
function setupTestableType() {
|
||||
const mockType = {
|
||||
name: 'slackApi',
|
||||
displayName: 'Slack API',
|
||||
properties: [],
|
||||
test: { request: { method: 'GET', url: '/test' } },
|
||||
};
|
||||
// @ts-expect-error Known pinia issue when spying on store getters
|
||||
vi.spyOn(credentialsStore, 'getCredentialTypeByName', 'get').mockReturnValue(() => mockType);
|
||||
}
|
||||
|
||||
test('writes error to store when getCredentialData throws', async () => {
|
||||
setupTestableType();
|
||||
vi.spyOn(credentialsStore, 'isCredentialTestedOk').mockReturnValue(false);
|
||||
vi.spyOn(credentialsStore, 'isCredentialTestPending').mockReturnValue(false);
|
||||
vi.spyOn(credentialsStore, 'getCredentialData').mockRejectedValue(new Error('Network error'));
|
||||
|
||||
const { testCredentialInBackground } = useCredentialTesting(() => null);
|
||||
await testCredentialInBackground('cred-1', 'My Slack', 'slackApi');
|
||||
|
||||
// Should write 'error' to prevent stuck spinner
|
||||
expect(credentialsStore.credentialTestResults.get('cred-1')).toBe('error');
|
||||
});
|
||||
|
||||
test('writes error to store when testCredential throws', async () => {
|
||||
setupTestableType();
|
||||
vi.spyOn(credentialsStore, 'isCredentialTestedOk').mockReturnValue(false);
|
||||
vi.spyOn(credentialsStore, 'isCredentialTestPending').mockReturnValue(false);
|
||||
vi.spyOn(credentialsStore, 'getCredentialData').mockResolvedValue({
|
||||
data: { apiKey: 'test-key' },
|
||||
} as never);
|
||||
vi.spyOn(credentialsStore, 'testCredential').mockRejectedValue(new Error('Test failed'));
|
||||
|
||||
const { testCredentialInBackground } = useCredentialTesting(() => null);
|
||||
await testCredentialInBackground('cred-1', 'My Slack', 'slackApi');
|
||||
|
||||
expect(credentialsStore.credentialTestResults.get('cred-1')).toBe('error');
|
||||
});
|
||||
|
||||
test('skips already-successful credentials', async () => {
|
||||
setupTestableType();
|
||||
vi.spyOn(credentialsStore, 'isCredentialTestedOk').mockReturnValue(true);
|
||||
const getDataSpy = vi.spyOn(credentialsStore, 'getCredentialData');
|
||||
|
||||
const { testCredentialInBackground } = useCredentialTesting(() => null);
|
||||
await testCredentialInBackground('cred-1', 'My Slack', 'slackApi');
|
||||
|
||||
expect(getDataSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('skips non-testable credential types', async () => {
|
||||
const mockType = { name: 'slackOAuth2Api', displayName: 'Slack OAuth2', properties: [] };
|
||||
// @ts-expect-error Known pinia issue when spying on store getters
|
||||
vi.spyOn(credentialsStore, 'getCredentialTypeByName', 'get').mockReturnValue(() => mockType);
|
||||
// @ts-expect-error Known pinia issue when spying on store getters
|
||||
vi.spyOn(credentialsStore, 'getNodesWithAccess', 'get').mockReturnValue(() => []);
|
||||
const getDataSpy = vi.spyOn(credentialsStore, 'getCredentialData');
|
||||
|
||||
const { testCredentialInBackground } = useCredentialTesting(() => null);
|
||||
await testCredentialInBackground('cred-1', 'My Slack', 'slackOAuth2Api');
|
||||
|
||||
expect(getDataSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCredTestIcon', () => {
|
||||
test('returns null when card has no credential type', () => {
|
||||
const { getCredTestIcon } = useCredentialTesting(() => null);
|
||||
const card = makeCard({ credentialType: undefined });
|
||||
expect(getCredTestIcon(card)).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null when no credential is selected', () => {
|
||||
const { getCredTestIcon } = useCredentialTesting(() => null);
|
||||
const card = makeCard();
|
||||
expect(getCredTestIcon(card)).toBeNull();
|
||||
});
|
||||
|
||||
test('returns check when credential test passed', () => {
|
||||
vi.spyOn(credentialsStore, 'isCredentialTestedOk').mockReturnValue(true);
|
||||
|
||||
const { getCredTestIcon } = useCredentialTesting(() => 'cred-1');
|
||||
const card = makeCard();
|
||||
expect(getCredTestIcon(card)).toBe('check');
|
||||
});
|
||||
|
||||
test('returns triangle-alert when credential test failed', () => {
|
||||
vi.spyOn(credentialsStore, 'isCredentialTestedOk').mockReturnValue(false);
|
||||
vi.spyOn(credentialsStore, 'isCredentialTestPending').mockReturnValue(false);
|
||||
credentialsStore.credentialTestResults.set('cred-1', 'error');
|
||||
|
||||
const { getCredTestIcon } = useCredentialTesting(() => 'cred-1');
|
||||
const card = makeCard();
|
||||
expect(getCredTestIcon(card)).toBe('triangle-alert');
|
||||
});
|
||||
|
||||
test('returns spinner when test is pending for testable type', () => {
|
||||
vi.spyOn(credentialsStore, 'isCredentialTestedOk').mockReturnValue(false);
|
||||
vi.spyOn(credentialsStore, 'isCredentialTestPending').mockReturnValue(false);
|
||||
const mockType = {
|
||||
name: 'slackApi',
|
||||
displayName: 'Slack API',
|
||||
properties: [],
|
||||
test: { request: { method: 'GET', url: '/test' } },
|
||||
};
|
||||
// @ts-expect-error Known pinia issue when spying on store getters
|
||||
vi.spyOn(credentialsStore, 'getCredentialTypeByName', 'get').mockReturnValue(() => mockType);
|
||||
|
||||
const { getCredTestIcon } = useCredentialTesting(() => 'cred-1');
|
||||
const card = makeCard();
|
||||
expect(getCredTestIcon(card)).toBe('spinner');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEffectiveCredTestResult', () => {
|
||||
test('trusts store result over backend result', () => {
|
||||
vi.spyOn(credentialsStore, 'isCredentialTestedOk').mockReturnValue(true);
|
||||
|
||||
const { getEffectiveCredTestResult } = useCredentialTesting(() => 'cred-1');
|
||||
const card = makeCard({
|
||||
credentialTestResult: { success: false, message: 'Backend says failed' },
|
||||
});
|
||||
const result = getEffectiveCredTestResult(card);
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
|
||||
test('falls back to backend result when selection matches original', () => {
|
||||
vi.spyOn(credentialsStore, 'isCredentialTestedOk').mockReturnValue(false);
|
||||
vi.spyOn(credentialsStore, 'isCredentialTestPending').mockReturnValue(false);
|
||||
|
||||
const { getEffectiveCredTestResult } = useCredentialTesting(() => 'cred-1');
|
||||
const card = makeCard({
|
||||
credentialTestResult: { success: true },
|
||||
});
|
||||
const result = getEffectiveCredTestResult(card);
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
|
||||
test('returns undefined when no result is available', () => {
|
||||
vi.spyOn(credentialsStore, 'isCredentialTestedOk').mockReturnValue(false);
|
||||
vi.spyOn(credentialsStore, 'isCredentialTestPending').mockReturnValue(false);
|
||||
|
||||
const { getEffectiveCredTestResult } = useCredentialTesting(() => 'new-cred');
|
||||
const card = makeCard();
|
||||
const result = getEffectiveCredTestResult(card);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,199 +0,0 @@
|
|||
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import type { InstanceAiWorkflowSetupNode } from '@n8n/api-types';
|
||||
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import {
|
||||
useWorkflowDocumentStore,
|
||||
createWorkflowDocumentId,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
import { useSetupCards } from '../composables/useSetupCards';
|
||||
|
||||
vi.mock('@/features/setupPanel/setupPanel.utils', () => ({
|
||||
getNodeParametersIssues: () => ({}),
|
||||
}));
|
||||
|
||||
function makeSetupNode(
|
||||
overrides: Partial<InstanceAiWorkflowSetupNode> = {},
|
||||
): InstanceAiWorkflowSetupNode {
|
||||
return {
|
||||
node: {
|
||||
name: 'DataTable',
|
||||
type: 'n8n-nodes-base.dataTable',
|
||||
typeVersion: 1,
|
||||
parameters: {},
|
||||
position: [0, 0] as [number, number],
|
||||
id: 'node-1',
|
||||
},
|
||||
isTrigger: false,
|
||||
...overrides,
|
||||
} as InstanceAiWorkflowSetupNode;
|
||||
}
|
||||
|
||||
function mockNodeType(properties: INodeTypeDescription['properties']) {
|
||||
const nodeType = { name: 'n8n-nodes-base.dataTable', properties } as INodeTypeDescription;
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
// @ts-expect-error Known pinia issue when spying on store getters
|
||||
vi.spyOn(nodeTypesStore, 'getNodeType', 'get').mockReturnValue(() => nodeType);
|
||||
}
|
||||
|
||||
describe('useSetupCards', () => {
|
||||
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
const pinia = createTestingPinia({ stubActions: false });
|
||||
setActivePinia(pinia);
|
||||
workflowsStore = useWorkflowsStore();
|
||||
workflowsStore.setWorkflowId('test-workflow');
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId('test-workflow'),
|
||||
);
|
||||
workflowDocumentStore.setNodes([
|
||||
{
|
||||
name: 'DataTable',
|
||||
type: 'n8n-nodes-base.dataTable',
|
||||
typeVersion: 1,
|
||||
parameters: {},
|
||||
position: [0, 0] as [number, number],
|
||||
id: 'node-1',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('param-issue card creation', () => {
|
||||
test('skips the card when the only outstanding issue is a nested param (fixedCollection)', () => {
|
||||
mockNodeType([
|
||||
{
|
||||
name: 'filters',
|
||||
displayName: 'Filters',
|
||||
type: 'fixedCollection',
|
||||
default: {},
|
||||
},
|
||||
]);
|
||||
|
||||
const setupRequests = ref<InstanceAiWorkflowSetupNode[]>([
|
||||
makeSetupNode({ parameterIssues: { filters: ['Filters are required'] } }),
|
||||
]);
|
||||
|
||||
const { cards } = useSetupCards(
|
||||
setupRequests,
|
||||
() => null,
|
||||
() => false,
|
||||
);
|
||||
|
||||
expect(cards.value).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('creates a card when the tracked issue maps to a simple (renderable) param', () => {
|
||||
mockNodeType([
|
||||
{
|
||||
name: 'tableName',
|
||||
displayName: 'Table Name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
]);
|
||||
|
||||
const setupRequests = ref<InstanceAiWorkflowSetupNode[]>([
|
||||
makeSetupNode({ parameterIssues: { tableName: ['Table Name is required'] } }),
|
||||
]);
|
||||
|
||||
const { cards } = useSetupCards(
|
||||
setupRequests,
|
||||
() => null,
|
||||
() => false,
|
||||
);
|
||||
|
||||
expect(cards.value).toHaveLength(1);
|
||||
expect(cards.value[0].hasParamIssues).toBe(true);
|
||||
});
|
||||
|
||||
test('creates a card when mixing simple and nested issues', () => {
|
||||
mockNodeType([
|
||||
{
|
||||
name: 'tableName',
|
||||
displayName: 'Table Name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
name: 'filters',
|
||||
displayName: 'Filters',
|
||||
type: 'fixedCollection',
|
||||
default: {},
|
||||
},
|
||||
]);
|
||||
|
||||
const setupRequests = ref<InstanceAiWorkflowSetupNode[]>([
|
||||
makeSetupNode({
|
||||
parameterIssues: {
|
||||
tableName: ['Table Name is required'],
|
||||
filters: ['Filters are required'],
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const { cards } = useSetupCards(
|
||||
setupRequests,
|
||||
() => null,
|
||||
() => false,
|
||||
);
|
||||
|
||||
expect(cards.value).toHaveLength(1);
|
||||
expect(cards.value[0].hasParamIssues).toBe(true);
|
||||
});
|
||||
|
||||
test('keeps the card for a trigger node even when its only issue is nested', () => {
|
||||
mockNodeType([
|
||||
{
|
||||
name: 'filters',
|
||||
displayName: 'Filters',
|
||||
type: 'fixedCollection',
|
||||
default: {},
|
||||
},
|
||||
]);
|
||||
|
||||
const setupRequests = ref<InstanceAiWorkflowSetupNode[]>([
|
||||
makeSetupNode({
|
||||
isTrigger: true,
|
||||
parameterIssues: { filters: ['Filters are required'] },
|
||||
}),
|
||||
]);
|
||||
|
||||
const { cards } = useSetupCards(
|
||||
setupRequests,
|
||||
() => null,
|
||||
() => false,
|
||||
);
|
||||
|
||||
expect(cards.value).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('skips the card when the tracked issue is a param with multipleValues: true', () => {
|
||||
mockNodeType([
|
||||
{
|
||||
name: 'conditions',
|
||||
displayName: 'Conditions',
|
||||
type: 'string',
|
||||
default: '',
|
||||
typeOptions: { multipleValues: true },
|
||||
},
|
||||
]);
|
||||
|
||||
const setupRequests = ref<InstanceAiWorkflowSetupNode[]>([
|
||||
makeSetupNode({ parameterIssues: { conditions: ['Conditions are required'] } }),
|
||||
]);
|
||||
|
||||
const { cards } = useSetupCards(
|
||||
setupRequests,
|
||||
() => null,
|
||||
() => false,
|
||||
);
|
||||
|
||||
expect(cards.value).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -50,7 +50,11 @@ function handleClick(e: MouseEvent) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<N8nCard :class="[$style.card, props.archived && $style.cardArchived]" @click="handleClick">
|
||||
<N8nCard
|
||||
data-test-id="instance-ai-artifact-card"
|
||||
:class="[$style.card, props.archived && $style.cardArchived]"
|
||||
@click="handleClick"
|
||||
>
|
||||
<template #prepend>
|
||||
<N8nIcon :icon="icon" size="large" :class="$style.icon" />
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import GatewayResourceDecision from './GatewayResourceDecision.vue';
|
|||
import InstanceAiCredentialSetup from './InstanceAiCredentialSetup.vue';
|
||||
import type { QuestionAnswer } from './InstanceAiQuestions.vue';
|
||||
import InstanceAiQuestions from './InstanceAiQuestions.vue';
|
||||
import InstanceAiWorkflowSetup from './InstanceAiWorkflowSetup.vue';
|
||||
import InstanceAiWorkflowSetup from '../workflowSetup/InstanceAiWorkflowSetup.vue';
|
||||
import ConfirmationPreview from './ConfirmationPreview.vue';
|
||||
import PlanReviewPanel, { type PlannedTaskArg } from './PlanReviewPanel.vue';
|
||||
|
||||
|
|
@ -304,10 +304,9 @@ function isAllGenericApproval(items: PendingConfirmationItem[]): boolean {
|
|||
:class="$style.confirmation"
|
||||
:request-id="chunk.item.toolCall.confirmation.requestId"
|
||||
:setup-requests="chunk.item.toolCall.confirmation.setupRequests!"
|
||||
:workflow-id="chunk.item.toolCall.confirmation.workflowId ?? ''"
|
||||
:message="chunk.item.toolCall.confirmation.message"
|
||||
:project-id="chunk.item.toolCall.confirmation.projectId"
|
||||
:credential-flow="chunk.item.toolCall.confirmation.credentialFlow"
|
||||
:workflow-id="chunk.item.toolCall.confirmation.workflowId"
|
||||
/>
|
||||
|
||||
<!-- Credential setup -->
|
||||
|
|
|
|||
|
|
@ -347,6 +347,7 @@ async function handleLater() {
|
|||
:suggested-credential-name="currentRequest.suggestedName"
|
||||
standalone
|
||||
hide-issues
|
||||
hide-ask-assistant
|
||||
@credential-selected="onCredentialSelected(currentRequest.credentialType, $event)"
|
||||
/>
|
||||
<N8nButton
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,186 +0,0 @@
|
|||
import type { ComputedRef } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import { getMainAuthField } from '@/app/utils/nodeTypesUtils';
|
||||
import { useCredentialsStore } from '@/features/credentials/credentials.store';
|
||||
import { credGroupKey, type SetupCard } from '../instanceAiWorkflowSetup.utils';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
|
||||
export function useCredentialGroupSelection(
|
||||
cards: ComputedRef<SetupCard[]>,
|
||||
testCredentialInBackground: (id: string, name: string, type: string) => Promise<void>,
|
||||
projectId?: string,
|
||||
) {
|
||||
const uiStore = useUIStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const credentialsStore = useCredentialsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
|
||||
// Shared credential selection keyed by credGroupKey — single source of truth
|
||||
// for all cards in the same credential group (including escalated per-node cards).
|
||||
const credGroupSelections = ref<Record<string, string | null>>({});
|
||||
|
||||
// Tracks which credential group key (or section node name) initiated a "create credential" action.
|
||||
const activeCredentialTarget = ref<{ groupKey: string; credentialType: string } | null>(null);
|
||||
|
||||
function initCredGroupSelections() {
|
||||
// Build a group map first so we scan ALL cards per group, not bail early.
|
||||
const groupMap = new Map<string, SetupCard[]>();
|
||||
for (const card of cards.value) {
|
||||
if (!card.credentialType) continue;
|
||||
const key = card.nodes[0] ? credGroupKey(card.nodes[0]) : card.credentialType;
|
||||
const existing = groupMap.get(key);
|
||||
if (existing) existing.push(card);
|
||||
else groupMap.set(key, [card]);
|
||||
}
|
||||
|
||||
for (const [key, groupCards] of groupMap) {
|
||||
// Search ALL cards in the group for an assigned credential
|
||||
let selectedId: string | null = null;
|
||||
|
||||
for (const card of groupCards) {
|
||||
for (const req of card.nodes) {
|
||||
const credId = req.node.credentials?.[card.credentialType!]?.id;
|
||||
if (credId) {
|
||||
selectedId = credId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (selectedId) break;
|
||||
}
|
||||
|
||||
if (!selectedId) {
|
||||
// Auto-select: check first card's existing credentials
|
||||
const firstCard = groupCards[0];
|
||||
const firstReq = firstCard.nodes[0];
|
||||
if (firstReq.existingCredentials?.length === 1) {
|
||||
selectedId = firstReq.existingCredentials[0].id;
|
||||
} else if (firstCard.isAutoApplied && firstReq.existingCredentials?.length) {
|
||||
selectedId = firstReq.existingCredentials[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
credGroupSelections.value[key] = selectedId;
|
||||
}
|
||||
}
|
||||
|
||||
function getCardCredentialId(card: SetupCard): string | null {
|
||||
if (!card.credentialType) return null;
|
||||
const key = card.nodes[0] ? credGroupKey(card.nodes[0]) : card.credentialType;
|
||||
return credGroupSelections.value[key] ?? null;
|
||||
}
|
||||
|
||||
function isFirstCardInCredGroup(card: SetupCard): boolean {
|
||||
if (!card.credentialType || !card.nodes[0]) return true;
|
||||
const key = credGroupKey(card.nodes[0]);
|
||||
return (
|
||||
cards.value.find((c) => c.credentialType && c.nodes[0] && credGroupKey(c.nodes[0]) === key)
|
||||
?.id === card.id
|
||||
);
|
||||
}
|
||||
|
||||
function setCredentialForGroup(groupKey: string, credentialType: string, credentialId: string) {
|
||||
// 1. Update shared group state
|
||||
credGroupSelections.value[groupKey] = credentialId;
|
||||
|
||||
// 2. Sync credential to workflow store nodes for ALL nodes in the group
|
||||
for (const c of cards.value) {
|
||||
if (!c.credentialType || !c.nodes[0]) continue;
|
||||
if (credGroupKey(c.nodes[0]) !== groupKey) continue;
|
||||
for (const req of c.nodes) {
|
||||
const storeNode = workflowDocumentStore.value.getNodeByName(req.node.name);
|
||||
if (storeNode) {
|
||||
const cred =
|
||||
req.existingCredentials?.find((cr) => cr.id === credentialId) ??
|
||||
credentialsStore.getCredentialById(credentialId);
|
||||
if (cred) {
|
||||
storeNode.credentials = {
|
||||
...storeNode.credentials,
|
||||
[credentialType]: { id: cred.id, name: cred.name },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Trigger background test
|
||||
const cred = credentialsStore.getCredentialById(credentialId);
|
||||
if (cred) {
|
||||
void testCredentialInBackground(credentialId, cred.name, credentialType);
|
||||
}
|
||||
}
|
||||
|
||||
function clearCredentialForGroup(groupKey: string, credentialType: string) {
|
||||
// 1. Clear shared group state
|
||||
credGroupSelections.value[groupKey] = null;
|
||||
|
||||
// 2. Remove credential from workflow store nodes for ALL nodes in the group
|
||||
for (const c of cards.value) {
|
||||
if (!c.credentialType || !c.nodes[0]) continue;
|
||||
if (credGroupKey(c.nodes[0]) !== groupKey) continue;
|
||||
for (const req of c.nodes) {
|
||||
const storeNode = workflowDocumentStore.value.getNodeByName(req.node.name);
|
||||
if (storeNode?.credentials?.[credentialType]) {
|
||||
const { [credentialType]: _removed, ...remaining } = storeNode.credentials;
|
||||
storeNode.credentials = remaining as typeof storeNode.credentials;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cardHasExistingCredentials(card: SetupCard): boolean {
|
||||
if (!card.credentialType) return false;
|
||||
const firstReq = card.nodes[0];
|
||||
return (
|
||||
(firstReq?.existingCredentials?.length ?? 0) > 0 ||
|
||||
(credentialsStore.getUsableCredentialByType(card.credentialType)?.length ?? 0) > 0
|
||||
);
|
||||
}
|
||||
|
||||
function findCardForGroup(credentialType: string, groupKey: string): SetupCard | undefined {
|
||||
return cards.value.find(
|
||||
(c) =>
|
||||
c.credentialType === credentialType && c.nodes[0] && credGroupKey(c.nodes[0]) === groupKey,
|
||||
);
|
||||
}
|
||||
|
||||
function shouldShowAuthOptions(card: SetupCard | undefined): boolean {
|
||||
const setupNode = card?.nodes[0]?.node;
|
||||
if (!setupNode) return false;
|
||||
const nodeType = nodeTypesStore.getNodeType(setupNode.type, setupNode.typeVersion);
|
||||
const mainAuthField = getMainAuthField(nodeType);
|
||||
return (
|
||||
mainAuthField !== null &&
|
||||
Array.isArray(mainAuthField.options) &&
|
||||
mainAuthField.options.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
function openNewCredentialForSection(credentialType: string, groupKey: string) {
|
||||
activeCredentialTarget.value = { groupKey, credentialType };
|
||||
const card = findCardForGroup(credentialType, groupKey);
|
||||
const showAuthOptions = shouldShowAuthOptions(card);
|
||||
const nodeName = card?.nodes[0]?.node.name;
|
||||
uiStore.openNewCredential(
|
||||
credentialType,
|
||||
showAuthOptions,
|
||||
false,
|
||||
projectId,
|
||||
undefined,
|
||||
nodeName,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
credGroupSelections,
|
||||
activeCredentialTarget,
|
||||
initCredGroupSelections,
|
||||
getCardCredentialId,
|
||||
isFirstCardInCredGroup,
|
||||
setCredentialForGroup,
|
||||
clearCredentialForGroup,
|
||||
cardHasExistingCredentials,
|
||||
openNewCredentialForSection,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
|
||||
import { useCredentialsStore } from '@/features/credentials/credentials.store';
|
||||
import type { SetupCard } from '../instanceAiWorkflowSetup.utils';
|
||||
|
||||
export function useCredentialTesting(getCardCredentialId: (card: SetupCard) => string | null) {
|
||||
const credentialsStore = useCredentialsStore();
|
||||
|
||||
function isCredentialTypeTestable(credentialTypeName: string): boolean {
|
||||
const credType = credentialsStore.getCredentialTypeByName(credentialTypeName);
|
||||
if (credType?.test) return true;
|
||||
const nodesWithAccess = credentialsStore.getNodesWithAccess(credentialTypeName);
|
||||
return nodesWithAccess.some((node) =>
|
||||
node.credentials?.some((cred) => cred.name === credentialTypeName && cred.testedBy),
|
||||
);
|
||||
}
|
||||
|
||||
async function testCredentialInBackground(
|
||||
credentialId: string,
|
||||
credentialName: string,
|
||||
credentialType: string,
|
||||
) {
|
||||
if (!isCredentialTypeTestable(credentialType)) return;
|
||||
if (
|
||||
credentialsStore.isCredentialTestedOk(credentialId) ||
|
||||
credentialsStore.isCredentialTestPending(credentialId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const credentialResponse = await credentialsStore.getCredentialData({ id: credentialId });
|
||||
if (!credentialResponse?.data || typeof credentialResponse.data === 'string') return;
|
||||
|
||||
// Re-check after the async fetch
|
||||
if (
|
||||
credentialsStore.isCredentialTestedOk(credentialId) ||
|
||||
credentialsStore.isCredentialTestPending(credentialId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
ownedBy: _ownedBy,
|
||||
sharedWithProjects: _sharedWithProjects,
|
||||
oauthTokenData,
|
||||
...data
|
||||
} = credentialResponse.data as Record<string, unknown>;
|
||||
|
||||
// OAuth credentials: token presence = success
|
||||
if (oauthTokenData) {
|
||||
credentialsStore.credentialTestResults.set(credentialId, 'success');
|
||||
return;
|
||||
}
|
||||
|
||||
await credentialsStore.testCredential({
|
||||
id: credentialId,
|
||||
name: credentialName,
|
||||
type: credentialType,
|
||||
data: data as ICredentialDataDecryptedObject,
|
||||
});
|
||||
} catch {
|
||||
// Ensure the credential is not stuck in a pending/unknown state —
|
||||
// write 'error' so the card shows a warning icon instead of an eternal spinner.
|
||||
credentialsStore.credentialTestResults.set(credentialId, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the credential test result for a card.
|
||||
* - Store result (keyed by credential ID): authoritative for any credential the client has tested
|
||||
* - Backend result (card.credentialTestResult): only valid when the selected credential
|
||||
* matches the original backend-assigned credential (unchanged selection)
|
||||
* - Returns undefined when no result is available yet (triggers spinner for testable types)
|
||||
*/
|
||||
function getEffectiveCredTestResult(
|
||||
card: SetupCard,
|
||||
): { success: boolean; message?: string } | undefined {
|
||||
const selectedId = getCardCredentialId(card);
|
||||
if (!selectedId) return undefined;
|
||||
|
||||
// 1. Store has a definitive result for this credential — always trust it
|
||||
if (credentialsStore.isCredentialTestedOk(selectedId)) {
|
||||
return { success: true };
|
||||
}
|
||||
if (credentialsStore.isCredentialTestPending(selectedId)) {
|
||||
return undefined; // in-progress
|
||||
}
|
||||
const storeResult = credentialsStore.credentialTestResults.get(selectedId);
|
||||
if (storeResult === 'error') {
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
// 2. Backend-provided result — only valid if the selection hasn't changed.
|
||||
const originalCredId = card.nodes[0]?.node.credentials?.[card.credentialType!]?.id;
|
||||
if (card.credentialTestResult && selectedId === originalCredId) {
|
||||
return card.credentialTestResult;
|
||||
}
|
||||
|
||||
// 3. No result available
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getCredTestIcon(card: SetupCard): 'spinner' | 'check' | 'triangle-alert' | null {
|
||||
if (!card.credentialType) return null;
|
||||
const selectedId = getCardCredentialId(card);
|
||||
if (!selectedId) return null;
|
||||
|
||||
const testResult = getEffectiveCredTestResult(card);
|
||||
if (testResult === undefined) {
|
||||
return isCredentialTypeTestable(card.credentialType) ? 'spinner' : null;
|
||||
}
|
||||
if (testResult.success) return 'check';
|
||||
return 'triangle-alert';
|
||||
}
|
||||
|
||||
return {
|
||||
isCredentialTypeTestable,
|
||||
testCredentialInBackground,
|
||||
getEffectiveCredTestResult,
|
||||
getCredTestIcon,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,338 +0,0 @@
|
|||
import type { ComputedRef, Ref } from 'vue';
|
||||
import { ref, watch, onUnmounted } from 'vue';
|
||||
import type { InstanceAiToolCallState, InstanceAiWorkflowSetupNode } from '@n8n/api-types';
|
||||
import { useNodeHelpers } from '@/app/composables/useNodeHelpers';
|
||||
import { useTelemetry } from '@/app/composables/useTelemetry';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import type { ThreadRuntime } from '../instanceAi.store';
|
||||
import type { DisplayCard, SetupCard } from '../instanceAiWorkflowSetup.utils';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
|
||||
export function useSetupActions(deps: {
|
||||
requestId: Ref<string>;
|
||||
thread: ThreadRuntime;
|
||||
cards: ComputedRef<SetupCard[]>;
|
||||
currentDisplayCard: ComputedRef<DisplayCard | undefined>;
|
||||
displayCards: ComputedRef<DisplayCard[]>;
|
||||
buildNodeCredentials: () => Record<string, Record<string, string>>;
|
||||
buildNodeParameters: () => Record<string, Record<string, unknown>> | undefined;
|
||||
isCardComplete: (card: SetupCard) => boolean;
|
||||
anyCardComplete: ComputedRef<boolean>;
|
||||
allPreResolved: ComputedRef<boolean>;
|
||||
showFullWizard: Ref<boolean>;
|
||||
setCredentialForGroup: (groupKey: string, credentialType: string, credentialId: string) => void;
|
||||
clearCredentialForGroup: (groupKey: string, credentialType: string) => void;
|
||||
goToNext: () => void;
|
||||
isNextDisabled: ComputedRef<boolean>;
|
||||
credGroupKey: (req: InstanceAiWorkflowSetupNode) => string;
|
||||
setupRequests: Ref<InstanceAiWorkflowSetupNode[]>;
|
||||
onApplySuccess?: () => void;
|
||||
}) {
|
||||
const telemetry = useTelemetry();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
|
||||
const isSubmitted = ref(false);
|
||||
const isDeferred = ref(false);
|
||||
const isPartial = ref(false);
|
||||
const isApplying = ref(false);
|
||||
const applyError = ref<string | null>(null);
|
||||
|
||||
function isToolResult(val: unknown): val is Record<string, unknown> {
|
||||
return typeof val === 'object' && val !== null && !Array.isArray(val);
|
||||
}
|
||||
|
||||
let cancelApplyWait: (() => void) | null = null;
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelApplyWait?.();
|
||||
});
|
||||
|
||||
function trackSetupInput() {
|
||||
const tc = deps.thread.findToolCallByRequestId(deps.requestId.value);
|
||||
const inputThreadId = tc?.confirmation?.inputThreadId ?? '';
|
||||
const provided: Array<{ label: string; options: string[]; option_chosen: string }> = [];
|
||||
const skipped: Array<{ label: string; options: string[] }> = [];
|
||||
for (const card of deps.cards.value) {
|
||||
const name = card.nodes[0]?.node.name ?? card.id;
|
||||
if (deps.isCardComplete(card)) {
|
||||
provided.push({ label: name, options: [], option_chosen: 'configured' });
|
||||
} else {
|
||||
skipped.push({ label: name, options: [] });
|
||||
}
|
||||
}
|
||||
telemetry.track('User finished providing input', {
|
||||
thread_id: deps.thread.currentThreadId,
|
||||
input_thread_id: inputThreadId,
|
||||
instance_id: useRootStore().instanceId,
|
||||
type: 'setup',
|
||||
provided_inputs: provided,
|
||||
skipped_inputs: skipped,
|
||||
num_tasks: deps.cards.value.length,
|
||||
});
|
||||
}
|
||||
|
||||
/** Watch for the tool-result SSE event and resolve when it arrives. */
|
||||
function waitForToolResult(
|
||||
requestId: string,
|
||||
timeoutMs = 60_000,
|
||||
): { promise: Promise<Record<string, unknown> | null>; cancel: () => void } {
|
||||
let stopWatch: (() => void) | null = null;
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const promise = new Promise<Record<string, unknown> | null>((resolve) => {
|
||||
const existing = deps.thread.findToolCallByRequestId(requestId);
|
||||
if (existing?.result !== undefined) {
|
||||
resolve(isToolResult(existing.result) ? existing.result : null);
|
||||
return;
|
||||
}
|
||||
|
||||
stopWatch = watch(
|
||||
() => {
|
||||
const tc: InstanceAiToolCallState | undefined =
|
||||
deps.thread.findToolCallByRequestId(requestId);
|
||||
return tc?.result;
|
||||
},
|
||||
(result) => {
|
||||
if (result !== undefined) {
|
||||
cleanup();
|
||||
resolve(isToolResult(result) ? result : null);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
cleanup();
|
||||
resolve(null);
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
function cleanup() {
|
||||
if (stopWatch) {
|
||||
stopWatch();
|
||||
stopWatch = null;
|
||||
}
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
}
|
||||
|
||||
return { promise, cancel: cleanup };
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the server's authoritative updatedNodes to the canvas.
|
||||
*/
|
||||
function applyServerResultToCanvas(toolResult: Record<string, unknown>) {
|
||||
const updatedNodes = toolResult.updatedNodes as
|
||||
| Array<{
|
||||
id: string;
|
||||
name?: string;
|
||||
type: string;
|
||||
typeVersion: number;
|
||||
position: [number, number];
|
||||
parameters?: Record<string, unknown>;
|
||||
credentials?: Record<string, { id?: string; name: string }>;
|
||||
}>
|
||||
| undefined;
|
||||
|
||||
if (!updatedNodes) return;
|
||||
|
||||
for (const serverNode of updatedNodes) {
|
||||
const canvasNode = workflowDocumentStore.value.getNodeByName(serverNode.name ?? '');
|
||||
if (!canvasNode) continue;
|
||||
|
||||
if (serverNode.credentials) {
|
||||
canvasNode.credentials = serverNode.credentials as INodeUi['credentials'];
|
||||
}
|
||||
if (serverNode.parameters) {
|
||||
canvasNode.parameters = serverNode.parameters as INodeUi['parameters'];
|
||||
}
|
||||
|
||||
if (serverNode.name) {
|
||||
nodeHelpers.updateNodeParameterIssuesByName(serverNode.name);
|
||||
nodeHelpers.updateNodeCredentialIssuesByName(serverNode.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleApply() {
|
||||
const nodeCredentials = deps.buildNodeCredentials();
|
||||
const nodeParameters = deps.buildNodeParameters();
|
||||
|
||||
trackSetupInput();
|
||||
|
||||
isApplying.value = true;
|
||||
applyError.value = null;
|
||||
|
||||
const postSuccess = await deps.thread.confirmAction(deps.requestId.value, {
|
||||
kind: 'setupWorkflowApply',
|
||||
nodeCredentials,
|
||||
nodeParameters,
|
||||
});
|
||||
|
||||
if (!postSuccess) {
|
||||
isApplying.value = false;
|
||||
applyError.value = 'Failed to send confirmation. Try again.';
|
||||
return;
|
||||
}
|
||||
|
||||
const { promise, cancel } = waitForToolResult(deps.requestId.value);
|
||||
cancelApplyWait = cancel;
|
||||
const toolResult = await promise;
|
||||
cancelApplyWait = null;
|
||||
|
||||
isApplying.value = false;
|
||||
|
||||
if (toolResult && toolResult.success === true) {
|
||||
applyServerResultToCanvas(toolResult);
|
||||
isSubmitted.value = true;
|
||||
isPartial.value = toolResult.partial === true;
|
||||
deps.onApplySuccess?.();
|
||||
deps.thread.resolveConfirmation(deps.requestId.value, 'approved');
|
||||
} else if (toolResult) {
|
||||
applyError.value = typeof toolResult.error === 'string' ? toolResult.error : 'Apply failed';
|
||||
} else {
|
||||
applyError.value = 'Apply timed out — please try again.';
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTestTrigger(nodeName: string) {
|
||||
const nodeCredentials = deps.buildNodeCredentials();
|
||||
const nodeParameters = deps.buildNodeParameters();
|
||||
|
||||
applyError.value = null;
|
||||
|
||||
const postSuccess = await deps.thread.confirmAction(deps.requestId.value, {
|
||||
kind: 'setupWorkflowTestTrigger',
|
||||
testTriggerNode: nodeName,
|
||||
nodeCredentials,
|
||||
nodeParameters,
|
||||
});
|
||||
|
||||
if (!postSuccess) {
|
||||
applyError.value = 'Failed to send trigger test request. Try again.';
|
||||
return;
|
||||
}
|
||||
|
||||
const { promise, cancel } = waitForToolResult(deps.requestId.value);
|
||||
cancelApplyWait = cancel;
|
||||
const toolResult = await promise;
|
||||
cancelApplyWait = null;
|
||||
|
||||
if (toolResult === null) {
|
||||
// Timeout — the backend likely re-suspended with a new confirmation-request
|
||||
// that will replace this component. Nothing to do here.
|
||||
} else if (typeof toolResult.error === 'string') {
|
||||
applyError.value = toolResult.error;
|
||||
} else if (toolResult.success !== true) {
|
||||
applyError.value = 'Trigger test failed';
|
||||
}
|
||||
}
|
||||
|
||||
async function handleContinue() {
|
||||
if ((!deps.allPreResolved.value || deps.showFullWizard.value) && !deps.isNextDisabled.value) {
|
||||
deps.goToNext();
|
||||
return;
|
||||
}
|
||||
|
||||
await handleApply();
|
||||
}
|
||||
|
||||
async function handleLater() {
|
||||
if (!deps.allPreResolved.value || deps.showFullWizard.value) {
|
||||
const dc = deps.currentDisplayCard.value;
|
||||
|
||||
if (dc?.type === 'single' && dc.card.credentialType && dc.card.nodes[0]) {
|
||||
const key = deps.credGroupKey(dc.card.nodes[0]);
|
||||
deps.clearCredentialForGroup(key, dc.card.credentialType);
|
||||
}
|
||||
|
||||
if (dc?.type === 'group') {
|
||||
// Clear credentials for all cards in the group (parent + subnodes)
|
||||
const allGroupCards = [
|
||||
...(dc.group.parentCard ? [dc.group.parentCard] : []),
|
||||
...dc.group.subnodeCards,
|
||||
];
|
||||
for (const groupCard of allGroupCards) {
|
||||
if (groupCard.credentialType && groupCard.nodes[0]) {
|
||||
const key = deps.credGroupKey(groupCard.nodes[0]);
|
||||
deps.clearCredentialForGroup(key, groupCard.credentialType);
|
||||
}
|
||||
}
|
||||
|
||||
if (!deps.isNextDisabled.value) {
|
||||
deps.goToNext();
|
||||
return;
|
||||
}
|
||||
if (deps.anyCardComplete.value) {
|
||||
void handleApply();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!deps.isNextDisabled.value) {
|
||||
deps.goToNext();
|
||||
return;
|
||||
}
|
||||
|
||||
if (deps.anyCardComplete.value) {
|
||||
void handleApply();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No cards completed at all (or confirm mode) — defer the whole setup
|
||||
trackSetupInput();
|
||||
isSubmitted.value = true;
|
||||
isDeferred.value = true;
|
||||
|
||||
const success = await deps.thread.confirmAction(deps.requestId.value, {
|
||||
kind: 'approval',
|
||||
approved: false,
|
||||
});
|
||||
if (success) {
|
||||
deps.thread.resolveConfirmation(deps.requestId.value, 'deferred');
|
||||
} else {
|
||||
isSubmitted.value = false;
|
||||
isDeferred.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onCredentialSelected(
|
||||
card: SetupCard,
|
||||
updateInfo: { properties: { credentials?: Record<string, unknown> } },
|
||||
) {
|
||||
if (!card.credentialType) return;
|
||||
const credentialData = updateInfo.properties.credentials?.[card.credentialType];
|
||||
const credentialId =
|
||||
typeof credentialData === 'object' &&
|
||||
credentialData !== null &&
|
||||
'id' in credentialData &&
|
||||
typeof credentialData.id === 'string'
|
||||
? credentialData.id
|
||||
: undefined;
|
||||
const key = card.nodes[0] ? deps.credGroupKey(card.nodes[0]) : card.credentialType;
|
||||
|
||||
if (credentialId) {
|
||||
deps.setCredentialForGroup(key, card.credentialType, credentialId);
|
||||
} else {
|
||||
deps.clearCredentialForGroup(key, card.credentialType);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isSubmitted,
|
||||
isDeferred,
|
||||
isPartial,
|
||||
isApplying,
|
||||
applyError,
|
||||
handleApply,
|
||||
handleContinue,
|
||||
handleLater,
|
||||
handleTestTrigger,
|
||||
onCredentialSelected,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
import type { ComputedRef, Ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import { hasPlaceholderDeep } from '@n8n/utils';
|
||||
import { NodeHelpers, type INodeProperties } from 'n8n-workflow';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import type { IUpdateInformation } from '@/Interface';
|
||||
import { isNestedParam, isParamValueSet, type SetupCard } from '../instanceAiWorkflowSetup.utils';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
|
||||
/** Check if the original node parameter value was a placeholder sentinel. */
|
||||
function isOriginalValuePlaceholder(req: SetupCard['nodes'][0], paramName: string): boolean {
|
||||
return hasPlaceholderDeep(req.node.parameters[paramName]);
|
||||
}
|
||||
|
||||
export function useSetupCardParameters(
|
||||
cards: ComputedRef<SetupCard[]>,
|
||||
trackedParamNames: Ref<Map<string, Set<string>>>,
|
||||
cardHasParamWork: (card: SetupCard) => boolean,
|
||||
) {
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
|
||||
const paramValues = ref<Record<string, Record<string, unknown>>>({});
|
||||
|
||||
/** Seed parameter values from existing node parameters for cards with param issues. */
|
||||
function initParamValues() {
|
||||
for (const card of cards.value) {
|
||||
if (!cardHasParamWork(card)) continue;
|
||||
const req = card.nodes[0];
|
||||
const nodeName = req.node.name;
|
||||
if (paramValues.value[nodeName]) continue;
|
||||
|
||||
const issueParamNames = Object.keys(req.parameterIssues ?? {});
|
||||
const nodeParams = req.node.parameters;
|
||||
const seeded: Record<string, unknown> = {};
|
||||
for (const paramName of issueParamNames) {
|
||||
const existing = nodeParams[paramName];
|
||||
if (isParamValueSet(existing)) {
|
||||
seeded[paramName] = existing;
|
||||
}
|
||||
}
|
||||
if (Object.keys(seeded).length > 0) {
|
||||
paramValues.value[nodeName] = seeded;
|
||||
}
|
||||
}
|
||||
}
|
||||
initParamValues();
|
||||
|
||||
function getCardParameters(card: SetupCard): INodeProperties[] {
|
||||
if (!cardHasParamWork(card)) return [];
|
||||
const req = card.nodes[0];
|
||||
const nodeType = nodeTypesStore.getNodeType(req.node.type, req.node.typeVersion);
|
||||
if (!nodeType?.properties) return [];
|
||||
|
||||
const nodeName = req.node.name;
|
||||
const tracked =
|
||||
trackedParamNames.value.get(nodeName) ?? new Set(Object.keys(req.parameterIssues ?? {}));
|
||||
const node = workflowDocumentStore.value.getNodeByName(nodeName);
|
||||
if (!node) return [];
|
||||
|
||||
return nodeType.properties.filter(
|
||||
(prop) =>
|
||||
tracked.has(prop.name) &&
|
||||
NodeHelpers.displayParameter(node.parameters, prop, node, nodeType),
|
||||
);
|
||||
}
|
||||
|
||||
function getCardSimpleParameters(card: SetupCard): INodeProperties[] {
|
||||
return getCardParameters(card).filter((p) => !isNestedParam(p));
|
||||
}
|
||||
|
||||
/** Set a parameter value. */
|
||||
function setParamValue(nodeName: string, paramName: string, value: unknown): void {
|
||||
if (!paramValues.value[nodeName]) {
|
||||
paramValues.value[nodeName] = {};
|
||||
}
|
||||
paramValues.value[nodeName][paramName] = value;
|
||||
}
|
||||
|
||||
/** Bridge ParameterInputList events to both local paramValues AND the workflow store node. */
|
||||
function onParameterValueChanged(card: SetupCard, parameterData: IUpdateInformation): void {
|
||||
const nodeName = card.nodes[0].node.name;
|
||||
const paramName = parameterData.name.replace(/^parameters\./, '');
|
||||
|
||||
// 1. Update local paramValues (used by buildNodeParameters on Apply)
|
||||
setParamValue(nodeName, paramName, parameterData.value);
|
||||
|
||||
// 2. Update workflow store node (needed for ParameterInputList reactivity,
|
||||
// dependent param resolution, and loadOptions calls)
|
||||
workflowDocumentStore.value.setNodeParameters(
|
||||
{ name: nodeName, value: { [paramName]: parameterData.value } },
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
/** Build nodeParameters from paramValues + store node (for NDV-edited params). */
|
||||
function buildNodeParameters(): Record<string, Record<string, unknown>> | undefined {
|
||||
const result: Record<string, Record<string, unknown>> = {};
|
||||
let hasValues = false;
|
||||
|
||||
for (const card of cards.value) {
|
||||
if (!cardHasParamWork(card)) continue;
|
||||
for (const req of card.nodes) {
|
||||
const nodeName = req.node.name;
|
||||
const paramNames =
|
||||
trackedParamNames.value.get(nodeName) ?? new Set(Object.keys(req.parameterIssues ?? {}));
|
||||
if (paramNames.size === 0) continue;
|
||||
const merged: Record<string, unknown> = {};
|
||||
for (const paramName of paramNames) {
|
||||
let val = paramValues.value[nodeName]?.[paramName];
|
||||
if (!isParamValueSet(val)) {
|
||||
val = workflowDocumentStore.value.getNodeByName(nodeName)?.parameters[paramName];
|
||||
}
|
||||
if (isParamValueSet(val)) {
|
||||
merged[paramName] = val;
|
||||
hasValues = true;
|
||||
} else if (isOriginalValuePlaceholder(req, paramName)) {
|
||||
// Explicitly send empty string to clear the placeholder sentinel on the backend
|
||||
merged[paramName] = '';
|
||||
hasValues = true;
|
||||
}
|
||||
}
|
||||
if (Object.keys(merged).length > 0) {
|
||||
result[nodeName] = merged;
|
||||
}
|
||||
}
|
||||
}
|
||||
return hasValues ? result : undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
paramValues,
|
||||
getCardParameters,
|
||||
getCardSimpleParameters,
|
||||
setParamValue,
|
||||
onParameterValueChanged,
|
||||
buildNodeParameters,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,434 +0,0 @@
|
|||
import type { Ref } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import type { InstanceAiWorkflowSetupNode } from '@n8n/api-types';
|
||||
import { hasPlaceholderDeep } from '@n8n/utils';
|
||||
import { NodeConnectionTypes, NodeHelpers } from 'n8n-workflow';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { useCredentialsStore } from '@/features/credentials/credentials.store';
|
||||
import { getNodeParametersIssues } from '@/features/setupPanel/setupPanel.utils';
|
||||
import {
|
||||
credGroupKey,
|
||||
isNestedParam,
|
||||
type DisplayCard,
|
||||
type SetupCard,
|
||||
type SetupCardGroup,
|
||||
} from '../instanceAiWorkflowSetup.utils';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
|
||||
export function useSetupCards(
|
||||
setupRequests: Ref<InstanceAiWorkflowSetupNode[]>,
|
||||
getCardCredentialId: (card: SetupCard) => string | null,
|
||||
isCredentialTypeTestable: (name: string) => boolean,
|
||||
) {
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const credentialsStore = useCredentialsStore();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tracked parameter names — grows over time as live validation discovers new fields
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const trackedParamNames = ref(new Map<string, Set<string>>());
|
||||
|
||||
function initTrackedParamNames() {
|
||||
for (const req of setupRequests.value) {
|
||||
if (req.parameterIssues && Object.keys(req.parameterIssues).length > 0) {
|
||||
trackedParamNames.value.set(req.node.name, new Set(Object.keys(req.parameterIssues)));
|
||||
}
|
||||
}
|
||||
}
|
||||
initTrackedParamNames();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Card grouping
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Credential group keys that need per-node splitting due to live-discovered param work.
|
||||
const liveEscalatedCredTypes = computed(() => {
|
||||
const escalated = new Set<string>();
|
||||
|
||||
const groupMembers = new Map<string, InstanceAiWorkflowSetupNode[]>();
|
||||
for (const req of setupRequests.value) {
|
||||
if (!req.credentialType) continue;
|
||||
const key = credGroupKey(req);
|
||||
const existing = groupMembers.get(key);
|
||||
if (existing) existing.push(req);
|
||||
else groupMembers.set(key, [req]);
|
||||
}
|
||||
|
||||
for (const [key, members] of groupMembers) {
|
||||
if (members.length <= 1) continue;
|
||||
for (const req of members) {
|
||||
if (trackedParamNames.value.has(req.node.name)) {
|
||||
escalated.add(key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return escalated;
|
||||
});
|
||||
|
||||
/**
|
||||
* True if at least one tracked parameter on this node resolves to a property
|
||||
* the wizard can render inline. The AI Assistant lives on its own route,
|
||||
* with the workflow canvas shown in an iframe — there is no NDV to fall back
|
||||
* to for nested-type issues. If nothing is renderable inline, the wizard
|
||||
* should skip the card and let the backend's re-analyze loop hand the
|
||||
* unresolved issue back to the LLM via `partial/skippedNodes`.
|
||||
*/
|
||||
function hasRenderableParamIssue(req: InstanceAiWorkflowSetupNode): boolean {
|
||||
const nodeType = nodeTypesStore.getNodeType(req.node.type, req.node.typeVersion);
|
||||
if (!nodeType?.properties) return false;
|
||||
|
||||
const node = workflowDocumentStore.value.getNodeByName(req.node.name);
|
||||
if (!node) return false;
|
||||
|
||||
const tracked =
|
||||
trackedParamNames.value.get(req.node.name) ?? new Set(Object.keys(req.parameterIssues ?? {}));
|
||||
if (tracked.size === 0) return false;
|
||||
|
||||
for (const prop of nodeType.properties) {
|
||||
if (!tracked.has(prop.name)) continue;
|
||||
if (isNestedParam(prop)) continue;
|
||||
if (!NodeHelpers.displayParameter(node.parameters, prop, node, nodeType)) continue;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const cards = computed((): SetupCard[] => {
|
||||
const escalatedCredTypes = new Set<string>();
|
||||
for (const req of setupRequests.value) {
|
||||
if (
|
||||
req.credentialType &&
|
||||
req.parameterIssues &&
|
||||
Object.keys(req.parameterIssues).length > 0
|
||||
) {
|
||||
escalatedCredTypes.add(credGroupKey(req));
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of liveEscalatedCredTypes.value) {
|
||||
escalatedCredTypes.add(key);
|
||||
}
|
||||
|
||||
const ordered: SetupCard[] = [];
|
||||
const credCardByKey = new Map<string, SetupCard>();
|
||||
|
||||
for (const req of setupRequests.value) {
|
||||
const hasParamIssues =
|
||||
req.parameterIssues !== undefined && Object.keys(req.parameterIssues).length > 0;
|
||||
|
||||
if (req.credentialType) {
|
||||
const key = credGroupKey(req);
|
||||
|
||||
if (escalatedCredTypes.has(key)) {
|
||||
ordered.push({
|
||||
id: `node-${req.node.id}`,
|
||||
credentialType: req.credentialType,
|
||||
nodes: [req],
|
||||
isTrigger: req.isTrigger,
|
||||
isFirstTrigger: req.isFirstTrigger ?? false,
|
||||
isTestable: req.isTestable ?? false,
|
||||
credentialTestResult: req.credentialTestResult,
|
||||
isAutoApplied: req.isAutoApplied ?? false,
|
||||
hasParamIssues,
|
||||
});
|
||||
} else {
|
||||
const existing = credCardByKey.get(key);
|
||||
if (existing) {
|
||||
existing.nodes.push(req);
|
||||
if (req.isTrigger) existing.isTrigger = true;
|
||||
if (req.isFirstTrigger) existing.isFirstTrigger = true;
|
||||
if (req.isTestable) existing.isTestable = true;
|
||||
if (req.isAutoApplied) existing.isAutoApplied = true;
|
||||
if (req.credentialTestResult && !existing.credentialTestResult) {
|
||||
existing.credentialTestResult = req.credentialTestResult;
|
||||
}
|
||||
} else {
|
||||
const card: SetupCard = {
|
||||
id: `cred-${key}`,
|
||||
credentialType: req.credentialType,
|
||||
nodes: [req],
|
||||
isTrigger: req.isTrigger,
|
||||
isFirstTrigger: req.isFirstTrigger ?? false,
|
||||
isTestable: req.isTestable ?? false,
|
||||
credentialTestResult: req.credentialTestResult,
|
||||
isAutoApplied: req.isAutoApplied ?? false,
|
||||
hasParamIssues: false,
|
||||
};
|
||||
credCardByKey.set(key, card);
|
||||
ordered.push(card);
|
||||
}
|
||||
}
|
||||
} else if (req.isTrigger || hasParamIssues) {
|
||||
// Skip param-only cards whose issues are all non-renderable (e.g. an
|
||||
// empty `fixedCollection`). There's nothing the user can edit inline,
|
||||
// and this wizard has no path to NDV. The backend will re-analyze on
|
||||
// Apply and feed the unresolved issue back to the LLM.
|
||||
if (!req.isTrigger && hasParamIssues && !hasRenderableParamIssue(req)) continue;
|
||||
|
||||
ordered.push({
|
||||
id: hasParamIssues ? `param-${req.node.id}` : `trigger-${req.node.id}`,
|
||||
nodes: [req],
|
||||
isTrigger: req.isTrigger,
|
||||
isFirstTrigger: req.isFirstTrigger ?? false,
|
||||
isTestable: req.isTestable ?? false,
|
||||
isAutoApplied: false,
|
||||
hasParamIssues,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return ordered;
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Display cards — groups AI parent nodes with their sub-nodes for rendering
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const displayCards = computed((): DisplayCard[] => {
|
||||
const directSubnodes = new Map<string, Set<string>>();
|
||||
const connectionsByDest = workflowDocumentStore.value.connectionsByDestinationNode;
|
||||
for (const [destName, conns] of Object.entries(connectionsByDest)) {
|
||||
for (const connType of Object.keys(conns)) {
|
||||
if (connType === NodeConnectionTypes.Main) continue;
|
||||
for (const group of conns[connType]) {
|
||||
if (!group) continue;
|
||||
for (const conn of group) {
|
||||
if (!directSubnodes.has(destName)) {
|
||||
directSubnodes.set(destName, new Set());
|
||||
}
|
||||
directSubnodes.get(destName)!.add(conn.node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const allSubnodeNames = new Set<string>();
|
||||
for (const subs of directSubnodes.values()) {
|
||||
for (const s of subs) allSubnodeNames.add(s);
|
||||
}
|
||||
|
||||
const rootParents = new Set<string>();
|
||||
for (const parentName of directSubnodes.keys()) {
|
||||
if (!allSubnodeNames.has(parentName)) {
|
||||
rootParents.add(parentName);
|
||||
}
|
||||
}
|
||||
|
||||
const transitiveSubnodes = new Map<string, Set<string>>();
|
||||
for (const root of rootParents) {
|
||||
const collected = new Set<string>();
|
||||
const queue = [...(directSubnodes.get(root) ?? [])];
|
||||
while (queue.length > 0) {
|
||||
const name = queue.pop()!;
|
||||
if (collected.has(name)) continue;
|
||||
collected.add(name);
|
||||
const children = directSubnodes.get(name);
|
||||
if (children) queue.push(...children);
|
||||
}
|
||||
if (collected.size > 0) {
|
||||
transitiveSubnodes.set(root, collected);
|
||||
}
|
||||
}
|
||||
|
||||
const cardsByNodeName = new Map<string, SetupCard>();
|
||||
for (const card of cards.value) {
|
||||
for (const req of card.nodes) {
|
||||
cardsByNodeName.set(req.node.name, card);
|
||||
}
|
||||
}
|
||||
|
||||
const usedCardIds = new Set<string>();
|
||||
const deferredSubnodeCardIds = new Set<string>();
|
||||
const result: DisplayCard[] = [];
|
||||
|
||||
for (const card of cards.value) {
|
||||
if (usedCardIds.has(card.id)) continue;
|
||||
|
||||
const primaryNodeName = card.nodes[0]?.node.name;
|
||||
const subnodeNames = primaryNodeName ? transitiveSubnodes.get(primaryNodeName) : undefined;
|
||||
|
||||
if (subnodeNames && subnodeNames.size > 0) {
|
||||
const subnodeCards: SetupCard[] = [];
|
||||
for (const subName of subnodeNames) {
|
||||
const subCard = cardsByNodeName.get(subName);
|
||||
if (subCard && !usedCardIds.has(subCard.id)) {
|
||||
subnodeCards.push(subCard);
|
||||
usedCardIds.add(subCard.id);
|
||||
deferredSubnodeCardIds.delete(subCard.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (subnodeCards.length > 0) {
|
||||
usedCardIds.add(card.id);
|
||||
result.push({
|
||||
type: 'group',
|
||||
group: {
|
||||
parentNode: card.nodes[0].node,
|
||||
parentCard: card,
|
||||
subnodeCards,
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let isSubnodeOfRoot = false;
|
||||
for (const [rootName, subs] of transitiveSubnodes) {
|
||||
if (primaryNodeName && subs.has(primaryNodeName)) {
|
||||
const rootCard = cardsByNodeName.get(rootName);
|
||||
if (rootCard && !usedCardIds.has(rootCard.id)) {
|
||||
isSubnodeOfRoot = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSubnodeOfRoot) {
|
||||
usedCardIds.add(card.id);
|
||||
result.push({ type: 'single', card });
|
||||
} else {
|
||||
// Track deferred subnodes so we can recover them if their root never collects them
|
||||
deferredSubnodeCardIds.add(card.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Emit any deferred subnode cards that were never consumed by a group
|
||||
for (const card of cards.value) {
|
||||
if (deferredSubnodeCardIds.has(card.id) && !usedCardIds.has(card.id)) {
|
||||
usedCardIds.add(card.id);
|
||||
result.push({ type: 'single', card });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Param work and completion
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function cardHasParamWork(card: SetupCard): boolean {
|
||||
for (const req of card.nodes) {
|
||||
const nodeName = req.node.name;
|
||||
if (trackedParamNames.value.has(nodeName)) return true;
|
||||
const storeNode = workflowDocumentStore.value.getNodeByName(nodeName);
|
||||
if (storeNode) {
|
||||
const liveIssues = getNodeParametersIssues(nodeTypesStore, storeNode);
|
||||
if (Object.keys(liveIssues).length > 0) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isCardComplete(card: SetupCard): boolean {
|
||||
if (card.credentialType) {
|
||||
const selectedId = getCardCredentialId(card);
|
||||
if (!selectedId) return false;
|
||||
|
||||
if (isCredentialTypeTestable(card.credentialType)) {
|
||||
if (!credentialsStore.isCredentialTestedOk(selectedId)) {
|
||||
const originalCredId = card.nodes[0]?.node.credentials?.[card.credentialType]?.id;
|
||||
if (!(card.credentialTestResult?.success && selectedId === originalCredId)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cardHasParamWork(card)) {
|
||||
for (const req of card.nodes) {
|
||||
const storeNode = workflowDocumentStore.value.getNodeByName(req.node.name);
|
||||
if (storeNode) {
|
||||
const liveIssues = getNodeParametersIssues(nodeTypesStore, storeNode);
|
||||
if (Object.keys(liveIssues).length > 0) return false;
|
||||
// Check for remaining placeholder values only in tracked parameters
|
||||
const tracked = trackedParamNames.value.get(req.node.name);
|
||||
if (tracked) {
|
||||
for (const paramName of tracked) {
|
||||
if (hasPlaceholderDeep(storeNode.parameters[paramName])) return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (card.isTestable && card.isTrigger && card.isFirstTrigger) {
|
||||
const triggerTestResults: Record<string, InstanceAiWorkflowSetupNode['triggerTestResult']> =
|
||||
{};
|
||||
for (const req of setupRequests.value) {
|
||||
if (req.triggerTestResult) {
|
||||
triggerTestResults[req.node.name] = req.triggerTestResult;
|
||||
}
|
||||
}
|
||||
const triggerNode = card.nodes.find((n) => n.isTrigger);
|
||||
const result = triggerNode ? triggerTestResults[triggerNode.node.name] : undefined;
|
||||
if (!result || result.status !== 'success') return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const anyCardComplete = computed(() => cards.value.some((c) => isCardComplete(c)));
|
||||
|
||||
const allPreResolved = computed(() => setupRequests.value.every((r) => !r.needsAction));
|
||||
|
||||
function isDisplayCardComplete(dc: DisplayCard): boolean {
|
||||
if (dc.type === 'single') return isCardComplete(dc.card);
|
||||
const { group } = dc;
|
||||
if (group.parentCard && !isCardComplete(group.parentCard)) return false;
|
||||
return group.subnodeCards.every((card) => isCardComplete(card));
|
||||
}
|
||||
|
||||
function getGroupPrimaryTriggerCard(group: SetupCardGroup): SetupCard | null {
|
||||
const allCards = group.parentCard
|
||||
? [group.parentCard, ...group.subnodeCards]
|
||||
: group.subnodeCards;
|
||||
return allCards.find((c) => c.isTestable && c.isTrigger && c.isFirstTrigger) ?? null;
|
||||
}
|
||||
|
||||
// Live parameter issue watcher — discovers new conditional required fields
|
||||
watch(
|
||||
() => {
|
||||
const result = new Map<string, string[]>();
|
||||
for (const card of cards.value) {
|
||||
for (const req of card.nodes) {
|
||||
const nodeName = req.node.name;
|
||||
const storeNode = workflowDocumentStore.value.getNodeByName(nodeName);
|
||||
if (!storeNode) continue;
|
||||
const liveIssues = getNodeParametersIssues(nodeTypesStore, storeNode);
|
||||
if (Object.keys(liveIssues).length > 0) {
|
||||
result.set(nodeName, Object.keys(liveIssues));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
(liveIssuesByNode) => {
|
||||
for (const [nodeName, issueNames] of liveIssuesByNode) {
|
||||
const existing = trackedParamNames.value.get(nodeName);
|
||||
if (existing) {
|
||||
for (const name of issueNames) existing.add(name);
|
||||
} else {
|
||||
trackedParamNames.value.set(nodeName, new Set(issueNames));
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
return {
|
||||
trackedParamNames,
|
||||
cards,
|
||||
displayCards,
|
||||
cardHasParamWork,
|
||||
isCardComplete,
|
||||
isDisplayCardComplete,
|
||||
anyCardComplete,
|
||||
allPreResolved,
|
||||
getGroupPrimaryTriggerCard,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
import type { InstanceAiWorkflowSetupNode } from '@n8n/api-types';
|
||||
import { isPlaceholderString } from '@n8n/utils';
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
import { isResourceLocatorValue } from 'n8n-workflow';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface SetupCard {
|
||||
id: string;
|
||||
credentialType?: string;
|
||||
nodes: InstanceAiWorkflowSetupNode[];
|
||||
isTrigger: boolean;
|
||||
isFirstTrigger: boolean;
|
||||
isTestable: boolean;
|
||||
credentialTestResult?: { success: boolean; message?: string };
|
||||
isAutoApplied: boolean;
|
||||
hasParamIssues: boolean;
|
||||
}
|
||||
|
||||
export interface SetupCardGroup {
|
||||
parentNode: InstanceAiWorkflowSetupNode['node'];
|
||||
parentCard?: SetupCard;
|
||||
subnodeCards: SetupCard[];
|
||||
}
|
||||
|
||||
export type DisplayCard =
|
||||
| { type: 'single'; card: SetupCard }
|
||||
| { type: 'group'; group: SetupCardGroup };
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const HTTP_REQUEST_NODE_TYPE = 'n8n-nodes-base.httpRequest';
|
||||
export const HTTP_REQUEST_TOOL_NODE_TYPE = 'n8n-nodes-base.httpRequestTool';
|
||||
|
||||
export const NESTED_PARAM_TYPES = new Set([
|
||||
'collection',
|
||||
'fixedCollection',
|
||||
'resourceMapper',
|
||||
'filter',
|
||||
'assignmentCollection',
|
||||
]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pure functions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function credGroupKey(req: InstanceAiWorkflowSetupNode): string {
|
||||
if (!req.credentialType) {
|
||||
return req.node.name;
|
||||
}
|
||||
const credType = req.credentialType;
|
||||
const isHttpRequest =
|
||||
req.node.type === HTTP_REQUEST_NODE_TYPE || req.node.type === HTTP_REQUEST_TOOL_NODE_TYPE;
|
||||
if (isHttpRequest) {
|
||||
const url = String(req.node.parameters.url ?? '');
|
||||
if (url.startsWith('=')) {
|
||||
return `${credType}:http:expr:${req.node.name}`;
|
||||
}
|
||||
return `${credType}:http:${url}`;
|
||||
}
|
||||
return credType;
|
||||
}
|
||||
|
||||
/** Check if a parameter value is meaningfully set (not empty, null, placeholder, or an empty resource locator). */
|
||||
export function isParamValueSet(val: unknown): boolean {
|
||||
if (val === undefined || val === null || val === '') return false;
|
||||
if (isPlaceholderString(val)) return false;
|
||||
if (isResourceLocatorValue(val)) {
|
||||
return (
|
||||
val.value !== '' &&
|
||||
val.value !== null &&
|
||||
val.value !== undefined &&
|
||||
!isPlaceholderString(val.value)
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isNestedParam(p: INodeProperties): boolean {
|
||||
return NESTED_PARAM_TYPES.has(p.type) || p.typeOptions?.multipleValues === true;
|
||||
}
|
||||
|
||||
export function toNodeUi(setupNode: InstanceAiWorkflowSetupNode): INodeUi {
|
||||
return {
|
||||
id: setupNode.node.id,
|
||||
name: setupNode.node.name,
|
||||
type: setupNode.node.type,
|
||||
typeVersion: setupNode.node.typeVersion,
|
||||
position: setupNode.node.position,
|
||||
parameters: setupNode.node.parameters as INodeUi['parameters'],
|
||||
...(setupNode.node.credentials !== undefined
|
||||
? { credentials: setupNode.node.credentials }
|
||||
: {}),
|
||||
} satisfies INodeUi;
|
||||
}
|
||||
|
||||
/** True when this card only has a trigger (no credentials and no param work) */
|
||||
export function isTriggerOnly(
|
||||
card: SetupCard,
|
||||
cardHasParamWork: (c: SetupCard) => boolean,
|
||||
): boolean {
|
||||
return card.isTrigger && !card.credentialType && !cardHasParamWork(card);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use credential icon only for multi-node credential-grouping cards, where the card
|
||||
* title is the credential display name. By construction (see `useSetupCards`), cards
|
||||
* with more than one node always have a credentialType.
|
||||
*/
|
||||
export function shouldUseCredentialIcon(card: SetupCard): boolean {
|
||||
return card.nodes.length > 1;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<script lang="ts" setup>
|
||||
import { toRef } from 'vue';
|
||||
import type { InstanceAiCredentialFlow, InstanceAiWorkflowSetupNode } from '@n8n/api-types';
|
||||
import WorkflowSetupWizard from './components/WorkflowSetupWizard.vue';
|
||||
import WorkflowSetupStatus from './components/WorkflowSetupStatus.vue';
|
||||
import { provideWorkflowSetupContext } from './composables/useWorkflowSetupContext';
|
||||
|
||||
const props = defineProps<{
|
||||
requestId: string;
|
||||
setupRequests: InstanceAiWorkflowSetupNode[];
|
||||
projectId?: string;
|
||||
workflowId?: string;
|
||||
credentialFlow?: InstanceAiCredentialFlow;
|
||||
}>();
|
||||
|
||||
const ctx = provideWorkflowSetupContext({
|
||||
requestId: toRef(props, 'requestId'),
|
||||
setupRequests: toRef(props, 'setupRequests'),
|
||||
projectId: toRef(props, 'projectId'),
|
||||
workflowId: toRef(props, 'workflowId'),
|
||||
credentialFlow: toRef(props, 'credentialFlow'),
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-test-id="instance-ai-workflow-setup">
|
||||
<WorkflowSetupStatus
|
||||
v-if="ctx.isReady.value && ctx.terminalState.value"
|
||||
:state="ctx.terminalState.value"
|
||||
/>
|
||||
<WorkflowSetupWizard v-else-if="ctx.isReady.value && ctx.steps.value.length" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import type { InstanceAiWorkflowSetupNode } from '@n8n/api-types';
|
||||
import type { WorkflowSetupSection } from '../workflowSetup.types';
|
||||
import { buildSectionId } from '../workflowSetup.helpers';
|
||||
|
||||
export function makeSetupRequest(
|
||||
overrides: Omit<Partial<InstanceAiWorkflowSetupNode>, 'node'> & {
|
||||
node?: Partial<InstanceAiWorkflowSetupNode['node']>;
|
||||
} = {},
|
||||
): InstanceAiWorkflowSetupNode {
|
||||
const { node: nodeOverrides, ...requestOverrides } = overrides;
|
||||
const node: InstanceAiWorkflowSetupNode['node'] = {
|
||||
id: 'http-request',
|
||||
name: 'HTTP Request',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4.2,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
return {
|
||||
credentialType: 'httpBasicAuth',
|
||||
existingCredentials: [],
|
||||
isTrigger: false,
|
||||
...requestOverrides,
|
||||
node: {
|
||||
...node,
|
||||
...nodeOverrides,
|
||||
},
|
||||
};
|
||||
}
|
||||
export function makeWorkflowSetupSection(
|
||||
overrides: Omit<Partial<WorkflowSetupSection>, 'node'> & {
|
||||
node?: Partial<WorkflowSetupSection['node']>;
|
||||
} = {},
|
||||
): WorkflowSetupSection {
|
||||
const node: WorkflowSetupSection['node'] = {
|
||||
id: 'http-request',
|
||||
name: 'HTTP Request',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4.2,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
const credentialType = 'credentialType' in overrides ? overrides.credentialType : 'httpBasicAuth';
|
||||
const targetNodeName = overrides.targetNodeName ?? overrides.node?.name ?? node.name;
|
||||
const finalNode = {
|
||||
...node,
|
||||
name: targetNodeName,
|
||||
...overrides.node,
|
||||
};
|
||||
|
||||
return {
|
||||
id: overrides.id ?? buildSectionId(targetNodeName, credentialType),
|
||||
...(credentialType ? { credentialType } : {}),
|
||||
targetNodeName,
|
||||
node: finalNode,
|
||||
currentCredentialId: overrides.currentCredentialId ?? null,
|
||||
parameterNames: overrides.parameterNames ?? [],
|
||||
credentialTargetNodes: overrides.credentialTargetNodes ?? [
|
||||
{ id: finalNode.id, name: finalNode.name, type: finalNode.type },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
import { computed, ref } from 'vue';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import WorkflowSetupCard from './WorkflowSetupCard.vue';
|
||||
import { makeWorkflowSetupSection } from '../__tests__/factories';
|
||||
import type { WorkflowSetupContext } from '../composables/useWorkflowSetupContext';
|
||||
import type { WorkflowSetupSection } from '../workflowSetup.types';
|
||||
|
||||
const workflowSetupContext = vi.hoisted(() => ({
|
||||
current: undefined as unknown as WorkflowSetupContext,
|
||||
}));
|
||||
|
||||
const credentialsStore = vi.hoisted(() => ({
|
||||
getCredentialTypeByName: vi.fn(),
|
||||
getCredentialById: vi.fn(),
|
||||
}));
|
||||
|
||||
const nodeTypesStore = vi.hoisted(() => ({
|
||||
getNodeType: vi.fn(),
|
||||
communityNodeType: vi.fn(() => null),
|
||||
}));
|
||||
|
||||
vi.mock('../composables/useWorkflowSetupContext', () => ({
|
||||
useWorkflowSetupContext: () => workflowSetupContext.current,
|
||||
}));
|
||||
|
||||
vi.mock('@n8n/i18n', async (importOriginal) => ({
|
||||
...(await importOriginal()),
|
||||
useI18n: () => ({
|
||||
baseText: (key: string, opts?: { interpolate?: Record<string, string> }) => {
|
||||
if (key === 'instanceAi.credential.setupTitle' && opts?.interpolate?.name) {
|
||||
return `Set up ${opts.interpolate.name}`;
|
||||
}
|
||||
return key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/features/credentials/credentials.store', () => ({
|
||||
useCredentialsStore: () => credentialsStore,
|
||||
}));
|
||||
|
||||
vi.mock('@/app/stores/nodeTypes.store', () => ({
|
||||
useNodeTypesStore: () => nodeTypesStore,
|
||||
}));
|
||||
|
||||
vi.mock('@/features/settings/environments.ee/environments.store', () => ({
|
||||
default: () => ({ variablesAsObject: {} }),
|
||||
}));
|
||||
|
||||
vi.mock('@/app/components/NodeIcon.vue', () => ({
|
||||
default: {
|
||||
props: ['nodeType', 'size'],
|
||||
template: '<span data-test-id="node-icon" />',
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/features/credentials/components/CredentialIcon.vue', () => ({
|
||||
default: {
|
||||
props: ['credentialTypeName', 'size'],
|
||||
template: '<span data-test-id="credential-icon" />',
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('./WorkflowSetupSectionBody.vue', () => ({
|
||||
default: {
|
||||
props: ['section'],
|
||||
template: '<div data-test-id="workflow-setup-section-body" />',
|
||||
},
|
||||
}));
|
||||
|
||||
const renderComponent = createComponentRenderer(WorkflowSetupCard);
|
||||
|
||||
function makeContext(section: WorkflowSetupSection): WorkflowSetupContext {
|
||||
return {
|
||||
sections: computed(() => [section]),
|
||||
steps: computed(() => [{ kind: 'section', section }]),
|
||||
currentStepIndex: ref(0),
|
||||
activeStep: computed(() => ({ kind: 'section', section })),
|
||||
hasOtherUnhandledSteps: computed(() => false),
|
||||
canAdvanceToNextIncomplete: computed(() => false),
|
||||
credentialSelections: ref({}),
|
||||
terminalState: ref(null),
|
||||
isReady: ref(true),
|
||||
projectId: computed(() => undefined),
|
||||
credentialFlow: computed(() => undefined),
|
||||
isActionPending: ref(false),
|
||||
setCredential: vi.fn(),
|
||||
setParameterValue: vi.fn(),
|
||||
getDisplayNode: (setupSection) => setupSection.node as INodeUi,
|
||||
isSectionComplete: () => false,
|
||||
isCredentialTestFailed: () => false,
|
||||
isSectionSkipped: () => false,
|
||||
isStepComplete: () => false,
|
||||
isStepSkipped: () => false,
|
||||
isStepHandled: () => false,
|
||||
goToStep: vi.fn(),
|
||||
goToNext: vi.fn(),
|
||||
goToPrev: vi.fn(),
|
||||
goToNextIncomplete: vi.fn(),
|
||||
apply: vi.fn(async () => {}),
|
||||
skipCurrentStep: vi.fn(async () => {}),
|
||||
};
|
||||
}
|
||||
|
||||
describe('WorkflowSetupCard', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
credentialsStore.getCredentialTypeByName.mockReturnValue({ displayName: 'Header Auth' });
|
||||
nodeTypesStore.getNodeType.mockReturnValue({
|
||||
name: 'n8n-nodes-base.httpRequest',
|
||||
displayName: 'HTTP Request',
|
||||
properties: [{ displayName: 'URL', name: 'url', type: 'string', default: '' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the node name when the section includes parameters and credentials', () => {
|
||||
const section = makeWorkflowSetupSection({
|
||||
credentialType: 'httpHeaderAuth',
|
||||
parameterNames: ['url'],
|
||||
});
|
||||
workflowSetupContext.current = makeContext(section);
|
||||
|
||||
const { getByText, getByTestId, queryByText, queryByTestId } = renderComponent({
|
||||
props: { section },
|
||||
});
|
||||
|
||||
expect(getByText('HTTP Request')).toBeInTheDocument();
|
||||
expect(queryByText('Set up Header Auth')).not.toBeInTheDocument();
|
||||
expect(getByTestId('node-icon')).toBeInTheDocument();
|
||||
expect(queryByTestId('credential-icon')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the credential app name when the section only needs credentials', () => {
|
||||
const section = makeWorkflowSetupSection({
|
||||
credentialType: 'httpHeaderAuth',
|
||||
});
|
||||
workflowSetupContext.current = makeContext(section);
|
||||
|
||||
const { getByText, getByTestId, queryByText, queryByTestId } = renderComponent({
|
||||
props: { section },
|
||||
});
|
||||
|
||||
expect(getByText('Set up Header Auth')).toBeInTheDocument();
|
||||
expect(queryByText('HTTP Request')).not.toBeInTheDocument();
|
||||
expect(getByTestId('credential-icon')).toBeInTheDocument();
|
||||
expect(queryByTestId('node-icon')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { N8nIcon, N8nText } from '@n8n/design-system';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { getAppNameFromCredType } from '@/app/utils/nodeTypesUtils';
|
||||
import NodeIcon from '@/app/components/NodeIcon.vue';
|
||||
import CredentialIcon from '@/features/credentials/components/CredentialIcon.vue';
|
||||
import { useCredentialsStore } from '@/features/credentials/credentials.store';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import type { WorkflowSetupSection } from '../workflowSetup.types';
|
||||
import { useWorkflowSetupContext } from '../composables/useWorkflowSetupContext';
|
||||
import WorkflowSetupSectionBody from './WorkflowSetupSectionBody.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
section: WorkflowSetupSection;
|
||||
}>();
|
||||
|
||||
const ctx = useWorkflowSetupContext();
|
||||
const i18n = useI18n();
|
||||
const credentialsStore = useCredentialsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
|
||||
const credentialType = computed(() => props.section.credentialType);
|
||||
|
||||
const isComplete = computed(() => ctx.isSectionComplete(props.section));
|
||||
const isSkipped = computed(() => ctx.isSectionSkipped(props.section));
|
||||
|
||||
const nodeType = computed(() =>
|
||||
nodeTypesStore.getNodeType(props.section.node.type, props.section.node.typeVersion),
|
||||
);
|
||||
|
||||
const isCredentialOnlySection = computed(
|
||||
() => !!credentialType.value && props.section.parameterNames.length === 0,
|
||||
);
|
||||
|
||||
const displayName = computed(() => {
|
||||
const credentialTypeName = credentialType.value;
|
||||
if (!isCredentialOnlySection.value || !credentialTypeName) return props.section.node.name;
|
||||
const raw =
|
||||
credentialsStore.getCredentialTypeByName(credentialTypeName)?.displayName ?? credentialTypeName;
|
||||
const appName = getAppNameFromCredType(raw);
|
||||
return i18n.baseText('instanceAi.credential.setupTitle', { interpolate: { name: appName } });
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.card" data-test-id="instance-ai-workflow-setup-card">
|
||||
<header :class="$style.header">
|
||||
<CredentialIcon
|
||||
v-if="isCredentialOnlySection"
|
||||
:credential-type-name="credentialType ?? null"
|
||||
:size="16"
|
||||
/>
|
||||
<NodeIcon v-else :node-type="nodeType" :size="16" />
|
||||
<N8nText :class="$style.title" size="medium" color="text-dark" bold>
|
||||
{{ displayName }}
|
||||
</N8nText>
|
||||
<N8nText
|
||||
v-if="isComplete"
|
||||
data-test-id="instance-ai-workflow-setup-card-check"
|
||||
:class="$style.statusLabel"
|
||||
size="medium"
|
||||
color="success"
|
||||
>
|
||||
<N8nIcon icon="check" size="large" />
|
||||
{{ i18n.baseText('generic.complete') }}
|
||||
</N8nText>
|
||||
<N8nText
|
||||
v-else-if="isSkipped"
|
||||
data-test-id="instance-ai-workflow-setup-card-skipped"
|
||||
:class="$style.statusLabel"
|
||||
size="medium"
|
||||
color="text-light"
|
||||
>
|
||||
<N8nIcon icon="arrow-right" size="large" />
|
||||
{{ i18n.baseText('instanceAi.workflowSetup.cardSkipped') }}
|
||||
</N8nText>
|
||||
</header>
|
||||
|
||||
<div :class="$style.bodyWrapper">
|
||||
<WorkflowSetupSectionBody :section="section" />
|
||||
</div>
|
||||
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing--sm);
|
||||
padding-top: var(--spacing--sm);
|
||||
border: var(--border);
|
||||
border-radius: var(--radius);
|
||||
background-color: var(--color--background--light-3);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing--2xs);
|
||||
padding: 0 var(--spacing--sm);
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.statusLabel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing--4xs);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bodyWrapper {
|
||||
padding: 0 var(--spacing--sm);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
import { computed, ref } from 'vue';
|
||||
import { fireEvent } from '@testing-library/vue';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import WorkflowSetupGroupCard from './WorkflowSetupGroupCard.vue';
|
||||
import { makeWorkflowSetupSection } from '../__tests__/factories';
|
||||
import type { WorkflowSetupContext } from '../composables/useWorkflowSetupContext';
|
||||
import type { WorkflowSetupGroup, WorkflowSetupSection } from '../workflowSetup.types';
|
||||
|
||||
const workflowSetupContext = vi.hoisted(() => ({
|
||||
current: undefined as unknown as WorkflowSetupContext,
|
||||
}));
|
||||
|
||||
const nodeTypesStore = vi.hoisted(() => ({
|
||||
getNodeType: vi.fn(),
|
||||
communityNodeType: vi.fn(() => null),
|
||||
}));
|
||||
|
||||
vi.mock('../composables/useWorkflowSetupContext', () => ({
|
||||
useWorkflowSetupContext: () => workflowSetupContext.current,
|
||||
}));
|
||||
|
||||
vi.mock('@/app/stores/nodeTypes.store', () => ({
|
||||
useNodeTypesStore: () => nodeTypesStore,
|
||||
}));
|
||||
|
||||
vi.mock('@n8n/i18n', async (importOriginal) => ({
|
||||
...(await importOriginal()),
|
||||
useI18n: () => ({
|
||||
baseText: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/app/components/NodeIcon.vue', () => ({
|
||||
default: {
|
||||
props: ['nodeType', 'size'],
|
||||
template: '<span data-test-id="node-icon" :data-node-type="nodeType?.name" />',
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('./WorkflowSetupSectionBody.vue', () => ({
|
||||
default: {
|
||||
props: ['section'],
|
||||
template: '<div data-test-id="workflow-setup-section-body" :data-section-id="section.id" />',
|
||||
},
|
||||
}));
|
||||
|
||||
const renderComponent = createComponentRenderer(WorkflowSetupGroupCard);
|
||||
|
||||
interface ContextOverrides {
|
||||
completedIds?: Set<string>;
|
||||
}
|
||||
|
||||
function makeContext(overrides: ContextOverrides = {}): WorkflowSetupContext {
|
||||
const completedIds = overrides.completedIds ?? new Set<string>();
|
||||
return {
|
||||
sections: computed(() => []),
|
||||
steps: computed(() => []),
|
||||
currentStepIndex: ref(0),
|
||||
activeStep: computed(() => undefined),
|
||||
hasOtherUnhandledSteps: computed(() => false),
|
||||
canAdvanceToNextIncomplete: computed(() => false),
|
||||
credentialSelections: ref({}),
|
||||
terminalState: ref(null),
|
||||
isReady: ref(true),
|
||||
projectId: computed(() => undefined),
|
||||
credentialFlow: computed(() => undefined),
|
||||
isActionPending: ref(false),
|
||||
setCredential: vi.fn(),
|
||||
setParameterValue: vi.fn(),
|
||||
getDisplayNode: (section) => section.node as INodeUi,
|
||||
isSectionComplete: (section: WorkflowSetupSection) => completedIds.has(section.id),
|
||||
isCredentialTestFailed: () => false,
|
||||
isSectionSkipped: () => false,
|
||||
isStepComplete: () => false,
|
||||
isStepSkipped: () => false,
|
||||
isStepHandled: () => false,
|
||||
goToStep: vi.fn(),
|
||||
goToNext: vi.fn(),
|
||||
goToPrev: vi.fn(),
|
||||
goToNextIncomplete: vi.fn(),
|
||||
apply: vi.fn(async () => {}),
|
||||
skipCurrentStep: vi.fn(async () => {}),
|
||||
};
|
||||
}
|
||||
|
||||
const subnodeRootNode = { name: 'Agent', type: 'agentType', typeVersion: 1, id: 'agent-1' };
|
||||
|
||||
describe('WorkflowSetupGroupCard', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
nodeTypesStore.getNodeType.mockImplementation((type: string) => ({
|
||||
name: type,
|
||||
properties: [],
|
||||
}));
|
||||
});
|
||||
|
||||
it('renders the root node header even when there is no root section', () => {
|
||||
const sub = makeWorkflowSetupSection({
|
||||
id: 'Model:openAiApi',
|
||||
targetNodeName: 'Model',
|
||||
});
|
||||
const group: WorkflowSetupGroup = {
|
||||
subnodeRootNode,
|
||||
subnodeSections: [sub],
|
||||
};
|
||||
workflowSetupContext.current = makeContext();
|
||||
|
||||
const { getByText, getAllByTestId } = renderComponent({ props: { group } });
|
||||
|
||||
expect(getByText('Agent')).toBeInTheDocument();
|
||||
const icons = getAllByTestId('node-icon');
|
||||
expect(icons[0].getAttribute('data-node-type')).toBe('agentType');
|
||||
});
|
||||
|
||||
it('renders the root section inline without a collapsible header', async () => {
|
||||
const rootSection = makeWorkflowSetupSection({
|
||||
id: 'Agent:openAiApi',
|
||||
targetNodeName: 'Agent',
|
||||
credentialType: 'openAiApi',
|
||||
});
|
||||
const sub = makeWorkflowSetupSection({
|
||||
id: 'Model:openAiApi',
|
||||
targetNodeName: 'Model',
|
||||
});
|
||||
const group: WorkflowSetupGroup = {
|
||||
subnodeRootNode,
|
||||
rootSection,
|
||||
subnodeSections: [sub],
|
||||
};
|
||||
workflowSetupContext.current = makeContext();
|
||||
|
||||
const { getAllByTestId, queryAllByTestId } = renderComponent({ props: { group } });
|
||||
|
||||
const sectionWrappers = getAllByTestId('instance-ai-workflow-setup-section');
|
||||
expect(sectionWrappers).toHaveLength(2);
|
||||
|
||||
// Only sub-node sections render a collapsible header.
|
||||
const sectionHeaders = queryAllByTestId('instance-ai-workflow-setup-section-header');
|
||||
expect(sectionHeaders).toHaveLength(1);
|
||||
|
||||
// Root body is always rendered; the (incomplete) sub-node is
|
||||
// auto-expanded since it is the first incomplete sub-node.
|
||||
const bodies = getAllByTestId('workflow-setup-section-body');
|
||||
const renderedIds = bodies.map((body) => body.getAttribute('data-section-id'));
|
||||
expect(renderedIds).toEqual(expect.arrayContaining(['Agent:openAiApi', 'Model:openAiApi']));
|
||||
expect(bodies).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('toggles sub-node sections via the section header', async () => {
|
||||
const rootSection = makeWorkflowSetupSection({
|
||||
id: 'Agent:openAiApi',
|
||||
targetNodeName: 'Agent',
|
||||
credentialType: 'openAiApi',
|
||||
});
|
||||
const sub = makeWorkflowSetupSection({
|
||||
id: 'Model:openAiApi',
|
||||
targetNodeName: 'Model',
|
||||
});
|
||||
const group: WorkflowSetupGroup = {
|
||||
subnodeRootNode,
|
||||
rootSection,
|
||||
subnodeSections: [sub],
|
||||
};
|
||||
workflowSetupContext.current = makeContext();
|
||||
|
||||
const { getAllByTestId } = renderComponent({ props: { group } });
|
||||
|
||||
// Initial state: root body always rendered + sub-node auto-expanded.
|
||||
let bodies = getAllByTestId('workflow-setup-section-body');
|
||||
expect(bodies).toHaveLength(2);
|
||||
|
||||
// Only the sub-node has a header; clicking it collapses the sub-node body.
|
||||
const headers = getAllByTestId('instance-ai-workflow-setup-section-header');
|
||||
expect(headers).toHaveLength(1);
|
||||
await fireEvent.click(headers[0]);
|
||||
|
||||
bodies = getAllByTestId('workflow-setup-section-body');
|
||||
expect(bodies).toHaveLength(1);
|
||||
expect(bodies[0].getAttribute('data-section-id')).toBe('Agent:openAiApi');
|
||||
});
|
||||
|
||||
it('shows the complete badge only when every section is complete', () => {
|
||||
const rootSection = makeWorkflowSetupSection({
|
||||
id: 'Agent:openAiApi',
|
||||
targetNodeName: 'Agent',
|
||||
credentialType: 'openAiApi',
|
||||
});
|
||||
const sub = makeWorkflowSetupSection({
|
||||
id: 'Model:openAiApi',
|
||||
targetNodeName: 'Model',
|
||||
});
|
||||
const group: WorkflowSetupGroup = {
|
||||
subnodeRootNode,
|
||||
rootSection,
|
||||
subnodeSections: [sub],
|
||||
};
|
||||
workflowSetupContext.current = makeContext({
|
||||
completedIds: new Set([rootSection.id, sub.id]),
|
||||
});
|
||||
|
||||
const { getByTestId } = renderComponent({ props: { group } });
|
||||
|
||||
expect(getByTestId('instance-ai-workflow-setup-group-card-check')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the footer slot', () => {
|
||||
const sub = makeWorkflowSetupSection({
|
||||
id: 'Model:openAiApi',
|
||||
targetNodeName: 'Model',
|
||||
});
|
||||
const group: WorkflowSetupGroup = {
|
||||
subnodeRootNode,
|
||||
subnodeSections: [sub],
|
||||
};
|
||||
workflowSetupContext.current = makeContext();
|
||||
|
||||
const { getByTestId } = renderComponent({
|
||||
props: { group },
|
||||
slots: { footer: '<div data-test-id="footer-slot">footer</div>' },
|
||||
});
|
||||
|
||||
expect(getByTestId('footer-slot')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, toRef } from 'vue';
|
||||
import { N8nIcon, N8nText } from '@n8n/design-system';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import NodeIcon from '@/app/components/NodeIcon.vue';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import type { WorkflowSetupGroup, WorkflowSetupSection } from '../workflowSetup.types';
|
||||
import { getGroupSections } from '../workflowSetup.helpers';
|
||||
import { useWorkflowSetupContext } from '../composables/useWorkflowSetupContext';
|
||||
import { useWorkflowSetupGroupSections } from '../composables/useWorkflowSetupGroupSections';
|
||||
import WorkflowSetupSectionBody from './WorkflowSetupSectionBody.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
group: WorkflowSetupGroup;
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
const ctx = useWorkflowSetupContext();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
|
||||
const groupRef = toRef(props, 'group');
|
||||
const { expandedSections, toggleSection } = useWorkflowSetupGroupSections(groupRef);
|
||||
|
||||
const subnodeRootNodeType = computed(() =>
|
||||
nodeTypesStore.getNodeType(
|
||||
props.group.subnodeRootNode.type,
|
||||
props.group.subnodeRootNode.typeVersion,
|
||||
),
|
||||
);
|
||||
|
||||
const isGroupComplete = computed(() => getGroupSections(props.group).every(ctx.isSectionComplete));
|
||||
|
||||
function getSectionNodeType(section: WorkflowSetupSection) {
|
||||
return nodeTypesStore.getNodeType(section.node.type, section.node.typeVersion);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.card" data-test-id="instance-ai-workflow-setup-group-card">
|
||||
<header :class="$style.header">
|
||||
<NodeIcon :node-type="subnodeRootNodeType" :size="16" />
|
||||
<N8nText :class="$style.title" size="medium" color="text-dark" bold>
|
||||
{{ group.subnodeRootNode.name }}
|
||||
</N8nText>
|
||||
<N8nText
|
||||
v-if="isGroupComplete"
|
||||
data-test-id="instance-ai-workflow-setup-group-card-check"
|
||||
:class="$style.completeLabel"
|
||||
size="medium"
|
||||
color="success"
|
||||
>
|
||||
<N8nIcon icon="check" size="large" />
|
||||
{{ i18n.baseText('generic.complete') }}
|
||||
</N8nText>
|
||||
</header>
|
||||
|
||||
<div :class="$style.sections">
|
||||
<div
|
||||
v-if="group.rootSection"
|
||||
:key="group.rootSection.id"
|
||||
:class="[$style.section, $style.rootSection]"
|
||||
data-test-id="instance-ai-workflow-setup-section"
|
||||
>
|
||||
<WorkflowSetupSectionBody :section="group.rootSection" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="section in group.subnodeSections"
|
||||
:key="section.id"
|
||||
:class="$style.section"
|
||||
data-test-id="instance-ai-workflow-setup-section"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
:class="$style.sectionHeader"
|
||||
:aria-expanded="!!expandedSections[section.id]"
|
||||
:aria-controls="`workflow-setup-section-body-${section.id}`"
|
||||
data-test-id="instance-ai-workflow-setup-section-header"
|
||||
@click="toggleSection(section.id)"
|
||||
>
|
||||
<N8nIcon
|
||||
v-if="ctx.isSectionComplete(section)"
|
||||
icon="check"
|
||||
size="small"
|
||||
:class="$style.sectionCheck"
|
||||
/>
|
||||
<NodeIcon v-else :node-type="getSectionNodeType(section)" :size="14" />
|
||||
<N8nText :class="$style.sectionTitle" size="medium" color="text-dark">
|
||||
{{ section.node.name }}
|
||||
</N8nText>
|
||||
<N8nIcon
|
||||
:class="$style.sectionChevron"
|
||||
:icon="expandedSections[section.id] ? 'chevrons-down-up' : 'chevrons-up-down'"
|
||||
size="large"
|
||||
color="text-light"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
v-if="expandedSections[section.id]"
|
||||
:id="`workflow-setup-section-body-${section.id}`"
|
||||
:class="$style.sectionContent"
|
||||
data-test-id="instance-ai-workflow-setup-section-body"
|
||||
>
|
||||
<WorkflowSetupSectionBody :section="section" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: var(--border);
|
||||
border-radius: var(--radius);
|
||||
background-color: var(--color--background--light-3);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing--2xs);
|
||||
padding: var(--spacing--sm);
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.completeLabel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing--4xs);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sections {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.section {
|
||||
border-top: var(--border);
|
||||
padding: var(--spacing--sm);
|
||||
}
|
||||
|
||||
// The root section renders inline as the group card's primary body and
|
||||
// belongs with the group header, so no separator is drawn above it.
|
||||
.rootSection {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
// The root section sits directly under the group header; collapse its top
|
||||
// padding so the header's own bottom padding dictates the gap.
|
||||
.rootSection:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing--2xs);
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
.sectionChevron {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover .sectionChevron,
|
||||
&:focus-visible .sectionChevron {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
flex: 1;
|
||||
font-weight: var(--font-weight--medium);
|
||||
}
|
||||
|
||||
.sectionCheck {
|
||||
color: var(--color--success);
|
||||
}
|
||||
|
||||
.sectionContent {
|
||||
padding-top: var(--spacing--xs);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, provide, ref, watch } from 'vue';
|
||||
import { N8nText, N8nTooltip } from '@n8n/design-system';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import NodeCredentials from '@/features/credentials/components/NodeCredentials.vue';
|
||||
import ParameterInputList from '@/features/ndv/parameters/components/ParameterInputList.vue';
|
||||
import { useCredentialsStore } from '@/features/credentials/credentials.store';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import useEnvironmentsStore from '@/features/settings/environments.ee/environments.store';
|
||||
import { ExpressionLocalResolveContextSymbol } from '@/app/constants';
|
||||
import { Workflow, type IConnections, type INodeProperties } from 'n8n-workflow';
|
||||
import type { ExpressionLocalResolveContext } from '@/app/types/expressions';
|
||||
import type { INodeUi, INodeUpdatePropertiesInformation, IUpdateInformation } from '@/Interface';
|
||||
import type { WorkflowSetupSection } from '../workflowSetup.types';
|
||||
import { useWorkflowSetupContext } from '../composables/useWorkflowSetupContext';
|
||||
|
||||
const props = defineProps<{
|
||||
section: WorkflowSetupSection;
|
||||
}>();
|
||||
|
||||
const ctx = useWorkflowSetupContext();
|
||||
const i18n = useI18n();
|
||||
const credentialsStore = useCredentialsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const environmentsStore = useEnvironmentsStore();
|
||||
|
||||
const credentialType = computed(() => props.section.credentialType);
|
||||
|
||||
const selectedCredentialId = computed(() =>
|
||||
credentialType.value
|
||||
? (ctx.credentialSelections.value[props.section.targetNodeName]?.[credentialType.value] ?? null)
|
||||
: null,
|
||||
);
|
||||
|
||||
const targetNodeNames = computed(() =>
|
||||
props.section.credentialTargetNodes.map((node) => node.name),
|
||||
);
|
||||
const targetNodeNamesTooltip = computed(() => targetNodeNames.value.join(', '));
|
||||
const usedByNodesLabel = computed(() =>
|
||||
i18n.baseText('instanceAi.workflowSetup.usedByNodes', {
|
||||
adjustToNumber: targetNodeNames.value.length,
|
||||
interpolate: { count: targetNodeNames.value.length },
|
||||
}),
|
||||
);
|
||||
|
||||
const nodeType = computed(() =>
|
||||
nodeTypesStore.getNodeType(props.section.node.type, props.section.node.typeVersion),
|
||||
);
|
||||
|
||||
const parameterDefinitions = computed<INodeProperties[]>(() => {
|
||||
if (!nodeType.value || props.section.parameterNames.length === 0) return [];
|
||||
const names = new Set(props.section.parameterNames);
|
||||
return nodeType.value.properties.filter((property) => names.has(property.name));
|
||||
});
|
||||
|
||||
const revealedIssues = ref(new Set<string>());
|
||||
|
||||
watch(
|
||||
() => props.section.id,
|
||||
() => {
|
||||
revealedIssues.value = new Set();
|
||||
},
|
||||
);
|
||||
|
||||
const hiddenIssuesInputs = computed(() =>
|
||||
parameterDefinitions.value
|
||||
.filter((param) => !revealedIssues.value.has(param.name))
|
||||
.map((param) => param.name),
|
||||
);
|
||||
|
||||
function revealParameterIssues(parameterName: string) {
|
||||
revealedIssues.value.add(parameterName);
|
||||
}
|
||||
|
||||
function getRootParameterName(parameterName: string) {
|
||||
return parameterName.split(/[.[\]]/)[0] ?? parameterName;
|
||||
}
|
||||
|
||||
const displayNode = computed<INodeUi>(() => {
|
||||
const node = ctx.getDisplayNode(props.section);
|
||||
if (!credentialType.value) return node;
|
||||
const cred = selectedCredentialId.value
|
||||
? credentialsStore.getCredentialById(selectedCredentialId.value)
|
||||
: undefined;
|
||||
return {
|
||||
...node,
|
||||
credentials: cred ? { [credentialType.value]: { id: cred.id, name: cred.name } } : {},
|
||||
} as INodeUi;
|
||||
});
|
||||
|
||||
const expressionContext = computed<ExpressionLocalResolveContext | undefined>(() => {
|
||||
const node = displayNode.value;
|
||||
const connections: IConnections = {};
|
||||
const workflow = new Workflow({
|
||||
id: 'instance-ai-workflow-setup',
|
||||
name: 'Instance AI workflow setup',
|
||||
nodes: [node],
|
||||
connections,
|
||||
active: false,
|
||||
nodeTypes: nodeTypesStore.getAllNodeTypes(),
|
||||
});
|
||||
|
||||
return {
|
||||
localResolve: true,
|
||||
envVars: environmentsStore.variablesAsObject,
|
||||
workflow,
|
||||
execution: null,
|
||||
nodeName: node.name,
|
||||
additionalKeys: {},
|
||||
connections,
|
||||
};
|
||||
});
|
||||
|
||||
provide(ExpressionLocalResolveContextSymbol, expressionContext);
|
||||
|
||||
function onCredentialSelected(update: INodeUpdatePropertiesInformation) {
|
||||
if (!credentialType.value) return;
|
||||
const data = update.properties.credentials?.[credentialType.value];
|
||||
ctx.setCredential(props.section, data?.id ?? null);
|
||||
}
|
||||
|
||||
function onParameterValueChanged(update: IUpdateInformation) {
|
||||
const parameterName = update.name.replace(/^parameters\./, '');
|
||||
ctx.setParameterValue(props.section, parameterName, update.value);
|
||||
revealParameterIssues(getRootParameterName(parameterName));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.body">
|
||||
<NodeCredentials
|
||||
v-if="credentialType"
|
||||
:node="displayNode"
|
||||
:override-cred-type="credentialType"
|
||||
:project-id="ctx.projectId.value"
|
||||
standalone
|
||||
hide-issues
|
||||
hide-ask-assistant
|
||||
@credential-selected="onCredentialSelected"
|
||||
>
|
||||
<template v-if="section.credentialTargetNodes.length > 1" #label-postfix>
|
||||
<N8nTooltip placement="top">
|
||||
<template #content>{{ targetNodeNamesTooltip }}</template>
|
||||
<N8nText
|
||||
size="small"
|
||||
color="text-light"
|
||||
data-test-id="instance-ai-workflow-setup-card-nodes-hint"
|
||||
>
|
||||
{{ usedByNodesLabel }}
|
||||
</N8nText>
|
||||
</N8nTooltip>
|
||||
</template>
|
||||
</NodeCredentials>
|
||||
|
||||
<div
|
||||
v-if="parameterDefinitions.length > 0"
|
||||
:class="$style.parameters"
|
||||
data-test-id="instance-ai-workflow-setup-parameters"
|
||||
>
|
||||
<ParameterInputList
|
||||
:parameters="parameterDefinitions"
|
||||
:node-values="{ parameters: displayNode.parameters }"
|
||||
:node="displayNode"
|
||||
path="parameters"
|
||||
:hide-delete="true"
|
||||
:hidden-issues-inputs="hiddenIssuesInputs"
|
||||
:remove-first-parameter-margin="true"
|
||||
:remove-last-parameter-margin="true"
|
||||
:options-overrides="{ hideExpressionSelector: true, hideFocusPanelButton: true }"
|
||||
@value-changed="onParameterValueChanged"
|
||||
@parameter-blur="revealParameterIssues"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing--xs);
|
||||
|
||||
:global(.node-credentials) {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.parameters {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { N8nIcon, N8nText, type IconName } from '@n8n/design-system';
|
||||
import { i18n, type BaseTextKey } from '@n8n/i18n';
|
||||
import type { TerminalState } from '../workflowSetup.types';
|
||||
|
||||
const props = defineProps<{ state: TerminalState }>();
|
||||
|
||||
type IconClass = 'iconLoading' | 'iconSuccess' | 'iconWarning' | 'iconMuted';
|
||||
type StatusView = {
|
||||
icon: IconName;
|
||||
iconClass: IconClass;
|
||||
labelKey: BaseTextKey;
|
||||
spin?: boolean;
|
||||
};
|
||||
|
||||
const STATUS_BY_STATE: Record<TerminalState, StatusView> = {
|
||||
applying: {
|
||||
icon: 'spinner',
|
||||
iconClass: 'iconLoading',
|
||||
labelKey: 'instanceAi.workflowSetup.applying',
|
||||
spin: true,
|
||||
},
|
||||
applied: {
|
||||
icon: 'check',
|
||||
iconClass: 'iconSuccess',
|
||||
labelKey: 'instanceAi.workflowSetup.applied',
|
||||
},
|
||||
partial: {
|
||||
icon: 'triangle-alert',
|
||||
iconClass: 'iconWarning',
|
||||
labelKey: 'instanceAi.workflowSetup.partiallyApplied',
|
||||
},
|
||||
deferred: {
|
||||
icon: 'arrow-right',
|
||||
iconClass: 'iconMuted',
|
||||
labelKey: 'instanceAi.workflowSetup.deferred',
|
||||
},
|
||||
};
|
||||
|
||||
const statusView = computed(() => STATUS_BY_STATE[props.state]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.status" :data-test-id="`instance-ai-workflow-setup-status-${state}`">
|
||||
<N8nIcon
|
||||
:icon="statusView.icon"
|
||||
size="small"
|
||||
:spin="!!statusView.spin"
|
||||
:class="$style[statusView.iconClass]"
|
||||
/>
|
||||
<N8nText size="small" color="text-light">
|
||||
{{ i18n.baseText(statusView.labelKey) }}
|
||||
</N8nText>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing--2xs);
|
||||
padding: var(--spacing--xs) var(--spacing--sm);
|
||||
border: var(--border);
|
||||
border-radius: var(--radius);
|
||||
background-color: var(--color--background--light-3);
|
||||
font-size: var(--font-size--2xs);
|
||||
}
|
||||
|
||||
.iconLoading {
|
||||
color: var(--color--text--tint-1);
|
||||
}
|
||||
|
||||
.iconSuccess {
|
||||
color: var(--color--success);
|
||||
}
|
||||
|
||||
.iconWarning {
|
||||
color: var(--color--warning);
|
||||
}
|
||||
|
||||
.iconMuted {
|
||||
color: var(--color--text--tint-2);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import WorkflowSetupCard from './WorkflowSetupCard.vue';
|
||||
import WorkflowSetupGroupCard from './WorkflowSetupGroupCard.vue';
|
||||
import WorkflowSetupWizardFooter from './WorkflowSetupWizardFooter.vue';
|
||||
import { useWorkflowSetupContext } from '../composables/useWorkflowSetupContext';
|
||||
|
||||
const ctx = useWorkflowSetupContext();
|
||||
|
||||
const activeGroup = computed(() =>
|
||||
ctx.activeStep.value?.kind === 'group' ? ctx.activeStep.value.group : undefined,
|
||||
);
|
||||
|
||||
const activeSection = computed(() =>
|
||||
ctx.activeStep.value?.kind === 'section' ? ctx.activeStep.value.section : undefined,
|
||||
);
|
||||
|
||||
const groupKey = computed(() => {
|
||||
return activeGroup.value ? `group:${activeGroup.value.subnodeRootNode.name}` : undefined;
|
||||
});
|
||||
|
||||
const sectionKey = computed(() => activeSection.value?.id);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WorkflowSetupGroupCard v-if="activeGroup" :key="groupKey" :group="activeGroup">
|
||||
<template #footer>
|
||||
<WorkflowSetupWizardFooter />
|
||||
</template>
|
||||
</WorkflowSetupGroupCard>
|
||||
<WorkflowSetupCard v-else-if="activeSection" :key="sectionKey" :section="activeSection">
|
||||
<template #footer>
|
||||
<WorkflowSetupWizardFooter />
|
||||
</template>
|
||||
</WorkflowSetupCard>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { N8nButton, N8nIconButton, N8nText, N8nTooltip } from '@n8n/design-system';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import ConfirmationFooter from '../../components/ConfirmationFooter.vue';
|
||||
import { useWorkflowSetupContext } from '../composables/useWorkflowSetupContext';
|
||||
import { getStepSections } from '../workflowSetup.helpers';
|
||||
|
||||
const ctx = useWorkflowSetupContext();
|
||||
const i18n = useI18n();
|
||||
|
||||
const totalSteps = computed(() => ctx.steps.value.length);
|
||||
const showArrows = computed(() => totalSteps.value > 1);
|
||||
const isPrevDisabled = computed(() => ctx.currentStepIndex.value === 0);
|
||||
const isNextDisabled = computed(() => ctx.currentStepIndex.value >= totalSteps.value - 1);
|
||||
|
||||
const isActiveStepHandled = computed(() =>
|
||||
ctx.activeStep.value ? ctx.isStepHandled(ctx.activeStep.value) : false,
|
||||
);
|
||||
|
||||
const isPrimaryActionBlockedByCredentialTest = computed(() => {
|
||||
const step = ctx.activeStep.value;
|
||||
if (!step) return false;
|
||||
|
||||
return getStepSections(step).some(
|
||||
(section) => !ctx.isSectionSkipped(section) && ctx.isCredentialTestFailed(section),
|
||||
);
|
||||
});
|
||||
|
||||
const isPrimaryActionDisabled = computed(
|
||||
() => ctx.activeStep.value !== undefined && !isActiveStepHandled.value,
|
||||
);
|
||||
|
||||
const isFinalize = computed(() => ctx.credentialFlow.value?.stage === 'finalize');
|
||||
|
||||
const showSkipButton = computed(
|
||||
() => ctx.activeStep.value !== undefined && !isActiveStepHandled.value,
|
||||
);
|
||||
const showContinueButton = computed(() => ctx.hasOtherUnhandledSteps.value);
|
||||
|
||||
const skipLabel = computed(() =>
|
||||
i18n.baseText(
|
||||
isFinalize.value ? 'instanceAi.credential.finalize.later' : 'instanceAi.workflowSetup.later',
|
||||
),
|
||||
);
|
||||
|
||||
const primaryActionLabel = computed(() => {
|
||||
if (showContinueButton.value) return i18n.baseText('instanceAi.credential.continueButton');
|
||||
|
||||
return i18n.baseText(
|
||||
isFinalize.value
|
||||
? 'instanceAi.credential.finalize.applyCredentials'
|
||||
: 'instanceAi.workflowSetup.apply',
|
||||
);
|
||||
});
|
||||
|
||||
const primaryActionTestId = computed(() =>
|
||||
showContinueButton.value
|
||||
? 'instance-ai-workflow-setup-continue'
|
||||
: 'instance-ai-workflow-setup-apply',
|
||||
);
|
||||
|
||||
function onPrimaryAction() {
|
||||
if (showContinueButton.value) {
|
||||
ctx.goToNextIncomplete();
|
||||
return;
|
||||
}
|
||||
|
||||
void ctx.apply();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfirmationFooter layout="row-between" bordered>
|
||||
<div :class="$style.nav">
|
||||
<N8nIconButton
|
||||
v-if="showArrows"
|
||||
variant="ghost"
|
||||
size="medium"
|
||||
icon="chevron-left"
|
||||
:disabled="isPrevDisabled"
|
||||
data-test-id="instance-ai-workflow-setup-prev"
|
||||
:aria-label="i18n.baseText('instanceAi.workflowSetup.prevStep')"
|
||||
@click="ctx.goToPrev"
|
||||
/>
|
||||
<N8nText size="small" color="text-light">
|
||||
{{
|
||||
i18n.baseText('instanceAi.workflowSetup.stepCounter', {
|
||||
interpolate: { current: ctx.currentStepIndex.value + 1, total: totalSteps },
|
||||
})
|
||||
}}
|
||||
</N8nText>
|
||||
<N8nIconButton
|
||||
v-if="showArrows"
|
||||
variant="ghost"
|
||||
size="medium"
|
||||
icon="chevron-right"
|
||||
:disabled="isNextDisabled"
|
||||
data-test-id="instance-ai-workflow-setup-next"
|
||||
:aria-label="i18n.baseText('instanceAi.workflowSetup.nextStep')"
|
||||
@click="ctx.goToNext"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div :class="$style.actions">
|
||||
<N8nButton
|
||||
v-if="showSkipButton"
|
||||
variant="outline"
|
||||
size="medium"
|
||||
:label="skipLabel"
|
||||
:disabled="ctx.isActionPending.value"
|
||||
data-test-id="instance-ai-workflow-setup-later"
|
||||
@click="ctx.skipCurrentStep"
|
||||
/>
|
||||
<N8nTooltip
|
||||
:disabled="!isPrimaryActionBlockedByCredentialTest"
|
||||
:content="i18n.baseText('instanceAi.workflowSetup.credentialTestFailedTooltip')"
|
||||
>
|
||||
<N8nButton
|
||||
size="medium"
|
||||
:label="primaryActionLabel"
|
||||
:disabled="isPrimaryActionDisabled"
|
||||
:data-test-id="primaryActionTestId"
|
||||
@click="onPrimaryAction"
|
||||
/>
|
||||
</N8nTooltip>
|
||||
</div>
|
||||
</ConfirmationFooter>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.nav {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
gap: var(--spacing--4xs);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing--2xs);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,510 @@
|
|||
import { computed, nextTick, ref, type ComputedRef, type Ref } from 'vue';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { WorkflowSetupSection, WorkflowSetupStep } from '../workflowSetup.types';
|
||||
import { makeWorkflowSetupSection } from '../__tests__/factories';
|
||||
import { useWorkflowSetupActions } from './useWorkflowSetupActions';
|
||||
|
||||
const telemetryTrack = vi.fn();
|
||||
vi.mock('@/app/composables/useTelemetry', () => ({
|
||||
useTelemetry: () => ({ track: telemetryTrack }),
|
||||
}));
|
||||
|
||||
const rootStoreState = { instanceId: 'instance-1' };
|
||||
vi.mock('@n8n/stores/useRootStore', () => ({
|
||||
useRootStore: () => rootStoreState,
|
||||
}));
|
||||
|
||||
interface Harness {
|
||||
sectionA: WorkflowSetupSection;
|
||||
sectionB: WorkflowSetupSection;
|
||||
sections: ComputedRef<WorkflowSetupSection[]>;
|
||||
steps: ComputedRef<WorkflowSetupStep[]>;
|
||||
currentStepIndex: Ref<number>;
|
||||
isReady: Ref<boolean>;
|
||||
activeStep: ComputedRef<WorkflowSetupStep | undefined>;
|
||||
completedSet: Set<string>;
|
||||
skippedSet: Set<string>;
|
||||
credentialSelections: Ref<Record<string, Record<string, string>>>;
|
||||
skippedSectionIds: Ref<Set<string>>;
|
||||
goToStep: ReturnType<typeof vi.fn>;
|
||||
apply: ReturnType<typeof vi.fn>;
|
||||
defer: ReturnType<typeof vi.fn>;
|
||||
markSectionSkipped: ReturnType<typeof vi.fn>;
|
||||
buildCompletedSetupPayload: ReturnType<typeof vi.fn>;
|
||||
thread: { currentThreadId: string; findToolCallByRequestId: ReturnType<typeof vi.fn> };
|
||||
actions: ReturnType<typeof useWorkflowSetupActions>;
|
||||
}
|
||||
|
||||
function setupHarness(): Harness {
|
||||
const sectionA = makeWorkflowSetupSection({
|
||||
id: 'A:typeA',
|
||||
targetNodeName: 'A',
|
||||
credentialType: 'typeA',
|
||||
});
|
||||
const sectionB = makeWorkflowSetupSection({
|
||||
id: 'B:typeB',
|
||||
targetNodeName: 'B',
|
||||
credentialType: 'typeB',
|
||||
});
|
||||
const sections = computed(() => [sectionA, sectionB]);
|
||||
const steps = computed<WorkflowSetupStep[]>(() => [
|
||||
{ kind: 'section', section: sectionA },
|
||||
{ kind: 'section', section: sectionB },
|
||||
]);
|
||||
const currentStepIndex = ref(0);
|
||||
const isReady = ref(true);
|
||||
const activeStep = computed<WorkflowSetupStep | undefined>(
|
||||
() => steps.value[currentStepIndex.value],
|
||||
);
|
||||
|
||||
const completedSet = new Set<string>();
|
||||
const skippedSet = new Set<string>();
|
||||
const credentialSelections = ref<Record<string, Record<string, string>>>({});
|
||||
const skippedSectionIds = ref<Set<string>>(skippedSet);
|
||||
|
||||
const goToStep = vi.fn((idx: number) => {
|
||||
currentStepIndex.value = idx;
|
||||
});
|
||||
const apply = vi.fn().mockResolvedValue(undefined);
|
||||
const defer = vi.fn().mockResolvedValue(undefined);
|
||||
const markSectionSkipped = vi.fn((section: WorkflowSetupSection) => {
|
||||
skippedSet.add(section.id);
|
||||
skippedSectionIds.value = new Set(skippedSet);
|
||||
});
|
||||
const buildCompletedSetupPayload = vi.fn(() => {
|
||||
const out: Record<string, Record<string, string>> = {};
|
||||
for (const section of sections.value) {
|
||||
if (completedSet.has(section.id) && section.credentialType) {
|
||||
out[section.targetNodeName] = {
|
||||
...(out[section.targetNodeName] ?? {}),
|
||||
[section.credentialType]: 'cred-id',
|
||||
};
|
||||
}
|
||||
}
|
||||
return { nodeCredentials: out };
|
||||
});
|
||||
|
||||
const thread = {
|
||||
currentThreadId: 'thread-1',
|
||||
findToolCallByRequestId: vi.fn(() => ({
|
||||
confirmation: { inputThreadId: 'input-thread-1' },
|
||||
})),
|
||||
};
|
||||
|
||||
const actions = useWorkflowSetupActions({
|
||||
requestId: ref('req-1'),
|
||||
sections,
|
||||
steps,
|
||||
activeStep,
|
||||
currentStepIndex,
|
||||
isReady,
|
||||
goToStep,
|
||||
inputs: {
|
||||
credentialSelections,
|
||||
isSectionComplete: (section) => completedSet.has(section.id),
|
||||
isSectionSkipped: (section) => skippedSet.has(section.id),
|
||||
markSectionSkipped,
|
||||
buildCompletedSetupPayload,
|
||||
},
|
||||
applyMachine: { apply, defer },
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
thread: thread as any,
|
||||
});
|
||||
|
||||
return {
|
||||
sectionA,
|
||||
sectionB,
|
||||
sections,
|
||||
steps,
|
||||
currentStepIndex,
|
||||
isReady,
|
||||
activeStep,
|
||||
completedSet,
|
||||
skippedSet,
|
||||
credentialSelections,
|
||||
skippedSectionIds,
|
||||
goToStep,
|
||||
apply,
|
||||
defer,
|
||||
markSectionSkipped,
|
||||
buildCompletedSetupPayload,
|
||||
thread,
|
||||
actions,
|
||||
};
|
||||
}
|
||||
|
||||
function getTelemetryCalls(eventName: string) {
|
||||
return telemetryTrack.mock.calls.filter(([event]) => event === eventName);
|
||||
}
|
||||
|
||||
describe('useWorkflowSetupActions', () => {
|
||||
beforeEach(() => {
|
||||
telemetryTrack.mockReset();
|
||||
});
|
||||
|
||||
it('tracks the active setup step when it is shown', () => {
|
||||
setupHarness();
|
||||
const shownPayload = getTelemetryCalls('Instance AI workflow setup step shown')[0]?.[1];
|
||||
|
||||
expect(telemetryTrack).toHaveBeenCalledWith(
|
||||
'Instance AI workflow setup step shown',
|
||||
expect.objectContaining({
|
||||
thread_id: 'thread-1',
|
||||
input_thread_id: 'input-thread-1',
|
||||
instance_id: 'instance-1',
|
||||
type: 'setup',
|
||||
request_id: 'req-1',
|
||||
step_index: 1,
|
||||
step_count: 2,
|
||||
step_kind: 'section',
|
||||
setup_inputs: [
|
||||
expect.objectContaining({
|
||||
input_type: 'credential',
|
||||
node_type: 'n8n-nodes-base.httpRequest',
|
||||
credential_type: 'typeA',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(shownPayload).toEqual(expect.not.objectContaining({ sections: expect.anything() }));
|
||||
expect(shownPayload.setup_inputs[0]).toEqual(
|
||||
expect.not.objectContaining({
|
||||
node_name: expect.anything(),
|
||||
credential_target_nodes: expect.anything(),
|
||||
label: expect.anything(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('marks the active section skipped and advances to the next unhandled step without calling the API', async () => {
|
||||
const h = setupHarness();
|
||||
|
||||
await h.actions.skipCurrentStep();
|
||||
|
||||
expect(h.markSectionSkipped).toHaveBeenCalledWith(h.sectionA);
|
||||
expect(h.goToStep).toHaveBeenCalledWith(1);
|
||||
expect(h.apply).not.toHaveBeenCalled();
|
||||
expect(h.defer).not.toHaveBeenCalled();
|
||||
expect(getTelemetryCalls('User finished providing input')).toHaveLength(0);
|
||||
expect(telemetryTrack).toHaveBeenCalledWith(
|
||||
'Instance AI workflow setup step handled',
|
||||
expect.objectContaining({
|
||||
outcome: 'skipped',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('routes through partial apply when terminal skip happens with at least one completion', async () => {
|
||||
const h = setupHarness();
|
||||
// section A is completed (with a real selection so telemetry can read it),
|
||||
// section B is the active one and the only unhandled step.
|
||||
h.completedSet.add(h.sectionA.id);
|
||||
h.credentialSelections.value = { A: { typeA: 'cred-id' } };
|
||||
h.currentStepIndex.value = 1;
|
||||
await nextTick();
|
||||
|
||||
await h.actions.skipCurrentStep();
|
||||
|
||||
expect(h.markSectionSkipped).toHaveBeenCalledWith(h.sectionB);
|
||||
expect(h.apply).toHaveBeenCalledWith({ nodeCredentials: { A: { typeA: 'cred-id' } } });
|
||||
expect(h.defer).not.toHaveBeenCalled();
|
||||
expect(getTelemetryCalls('User finished providing input')).toHaveLength(1);
|
||||
expect(telemetryTrack).toHaveBeenCalledWith(
|
||||
'User finished providing input',
|
||||
expect.objectContaining({
|
||||
type: 'setup',
|
||||
explicitly_skipped_inputs: [{ label: 'n8n-nodes-base.httpRequest - typeB', options: [] }],
|
||||
provided_inputs: [
|
||||
expect.objectContaining({
|
||||
label: 'n8n-nodes-base.httpRequest - typeA',
|
||||
option_chosen: 'true',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('routes through defer when terminal skip happens with zero completions', async () => {
|
||||
const h = setupHarness();
|
||||
// pre-skip section A so section B is the only unhandled step on a fresh skip.
|
||||
h.skippedSet.add(h.sectionA.id);
|
||||
h.skippedSectionIds.value = new Set(h.skippedSet);
|
||||
h.currentStepIndex.value = 1;
|
||||
await nextTick();
|
||||
|
||||
await h.actions.skipCurrentStep();
|
||||
|
||||
expect(h.markSectionSkipped).toHaveBeenCalledWith(h.sectionB);
|
||||
expect(h.defer).toHaveBeenCalledTimes(1);
|
||||
expect(h.apply).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not skip a section with selected-but-not-complete credential into a partial apply', async () => {
|
||||
const h = setupHarness();
|
||||
// pre-skip section A so section B is the active and only unhandled step,
|
||||
// section B has a selection that hasn't completed (e.g. cred test pending).
|
||||
h.skippedSet.add(h.sectionA.id);
|
||||
h.skippedSectionIds.value = new Set(h.skippedSet);
|
||||
h.credentialSelections.value = { B: { typeB: 'cred-id-pending' } };
|
||||
h.currentStepIndex.value = 1;
|
||||
await nextTick();
|
||||
|
||||
await h.actions.skipCurrentStep();
|
||||
|
||||
// B is not complete, A is skipped → no completed sections → defer path.
|
||||
expect(h.defer).toHaveBeenCalledTimes(1);
|
||||
expect(h.apply).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('routes back to an earlier unhandled step when current is later', async () => {
|
||||
const h = setupHarness();
|
||||
// User manually navigated to step B without handling step A.
|
||||
h.currentStepIndex.value = 1;
|
||||
await nextTick();
|
||||
|
||||
await h.actions.skipCurrentStep();
|
||||
|
||||
expect(h.markSectionSkipped).toHaveBeenCalledWith(h.sectionB);
|
||||
// Step A is still unhandled → wizard should route back to it.
|
||||
expect(h.goToStep).toHaveBeenCalledWith(0);
|
||||
expect(h.apply).not.toHaveBeenCalled();
|
||||
expect(h.defer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('guards against rapid double-clicks via isActionPending', async () => {
|
||||
const h = setupHarness();
|
||||
// Make apply take a tick.
|
||||
let resolveApply: () => void = () => {};
|
||||
h.apply.mockReturnValueOnce(
|
||||
new Promise<void>((r) => {
|
||||
resolveApply = r;
|
||||
}),
|
||||
);
|
||||
h.completedSet.add(h.sectionA.id);
|
||||
h.currentStepIndex.value = 1;
|
||||
await nextTick();
|
||||
|
||||
// First call → terminal skip, schedules apply (pending).
|
||||
const first = h.actions.skipCurrentStep();
|
||||
// Concurrent call should be ignored.
|
||||
const second = h.actions.skipCurrentStep();
|
||||
|
||||
await second; // returns immediately — guard short-circuits
|
||||
expect(h.apply).toHaveBeenCalledTimes(1);
|
||||
|
||||
resolveApply();
|
||||
await first;
|
||||
expect(h.apply).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('nextUnhandledIndex skips both complete and skipped steps', async () => {
|
||||
const h = setupHarness();
|
||||
h.completedSet.add(h.sectionA.id);
|
||||
h.skippedSet.add(h.sectionB.id);
|
||||
h.skippedSectionIds.value = new Set(h.skippedSet);
|
||||
await nextTick();
|
||||
|
||||
expect(h.actions.nextUnhandledIndex.value).toBe(-1);
|
||||
expect(h.actions.hasOtherUnhandledSteps.value).toBe(false);
|
||||
});
|
||||
|
||||
it('apply() reports completed sections via partial credential map and tracks telemetry', async () => {
|
||||
const h = setupHarness();
|
||||
h.completedSet.add(h.sectionA.id);
|
||||
|
||||
await h.actions.apply();
|
||||
|
||||
expect(h.apply).toHaveBeenCalledWith({ nodeCredentials: { A: { typeA: 'cred-id' } } });
|
||||
expect(getTelemetryCalls('User finished providing input')).toHaveLength(1);
|
||||
expect(telemetryTrack).toHaveBeenCalledWith(
|
||||
'Instance AI workflow setup step handled',
|
||||
expect.objectContaining({
|
||||
outcome: 'completed',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('tracks both credential and parameter inputs for a completed mixed section', async () => {
|
||||
const h = setupHarness();
|
||||
h.sectionA.parameterNames = ['url', 'method'];
|
||||
h.completedSet.add(h.sectionA.id);
|
||||
h.credentialSelections.value = { A: { typeA: 'cred-id' } };
|
||||
|
||||
await h.actions.apply();
|
||||
|
||||
expect(telemetryTrack).toHaveBeenCalledWith(
|
||||
'User finished providing input',
|
||||
expect.objectContaining({
|
||||
provided_inputs: [
|
||||
{
|
||||
label: 'n8n-nodes-base.httpRequest - typeA',
|
||||
options: [],
|
||||
option_chosen: 'true',
|
||||
},
|
||||
{
|
||||
label: 'n8n-nodes-base.httpRequest - url',
|
||||
options: [],
|
||||
option_chosen: 'true',
|
||||
},
|
||||
{
|
||||
label: 'n8n-nodes-base.httpRequest - method',
|
||||
options: [],
|
||||
option_chosen: 'true',
|
||||
},
|
||||
],
|
||||
num_tasks: 2,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe('group steps', () => {
|
||||
it('skips only incomplete sections in a group, preserving complete ones', async () => {
|
||||
const sectionA = makeWorkflowSetupSection({
|
||||
id: 'Sub1:credA',
|
||||
targetNodeName: 'Sub1',
|
||||
credentialType: 'credA',
|
||||
});
|
||||
const sectionB = makeWorkflowSetupSection({
|
||||
id: 'Sub2:credB',
|
||||
targetNodeName: 'Sub2',
|
||||
credentialType: 'credB',
|
||||
});
|
||||
const sections = computed(() => [sectionA, sectionB]);
|
||||
const steps = computed<WorkflowSetupStep[]>(() => [
|
||||
{
|
||||
kind: 'group',
|
||||
group: {
|
||||
subnodeRootNode: { name: 'Agent', type: 'agent', typeVersion: 1, id: 'agent-1' },
|
||||
subnodeSections: [sectionA, sectionB],
|
||||
},
|
||||
},
|
||||
]);
|
||||
const currentStepIndex = ref(0);
|
||||
const activeStep = computed<WorkflowSetupStep | undefined>(() => steps.value[0]);
|
||||
|
||||
const completedSet = new Set<string>([sectionA.id]);
|
||||
const skippedSet = new Set<string>();
|
||||
const credentialSelections = ref<Record<string, Record<string, string>>>({
|
||||
Sub1: { credA: 'cred-a' },
|
||||
});
|
||||
const skippedSectionIds = ref<Set<string>>(skippedSet);
|
||||
const isReady = ref(true);
|
||||
const goToStep = vi.fn();
|
||||
const apply = vi.fn().mockResolvedValue(undefined);
|
||||
const defer = vi.fn().mockResolvedValue(undefined);
|
||||
const markSectionSkipped = vi.fn((section: WorkflowSetupSection) => {
|
||||
skippedSet.add(section.id);
|
||||
skippedSectionIds.value = new Set(skippedSet);
|
||||
});
|
||||
const buildCompletedSetupPayload = vi.fn(() => ({
|
||||
nodeCredentials: { Sub1: { credA: 'cred-a' } },
|
||||
}));
|
||||
|
||||
const thread = {
|
||||
currentThreadId: 'thread-1',
|
||||
findToolCallByRequestId: vi.fn(() => ({
|
||||
confirmation: { inputThreadId: 'input-thread-1' },
|
||||
})),
|
||||
};
|
||||
|
||||
const actions = useWorkflowSetupActions({
|
||||
requestId: ref('req-1'),
|
||||
sections,
|
||||
steps,
|
||||
activeStep,
|
||||
currentStepIndex,
|
||||
isReady,
|
||||
goToStep,
|
||||
inputs: {
|
||||
credentialSelections,
|
||||
isSectionComplete: (section) => completedSet.has(section.id),
|
||||
isSectionSkipped: (section) => skippedSet.has(section.id),
|
||||
markSectionSkipped,
|
||||
buildCompletedSetupPayload,
|
||||
},
|
||||
applyMachine: { apply, defer },
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
thread: thread as any,
|
||||
});
|
||||
|
||||
await actions.skipCurrentStep();
|
||||
|
||||
// Only the incomplete sectionB was marked skipped — sectionA remains complete.
|
||||
expect(markSectionSkipped).toHaveBeenCalledTimes(1);
|
||||
expect(markSectionSkipped).toHaveBeenCalledWith(sectionB);
|
||||
// Terminal — only one step. apply runs since at least one section is complete.
|
||||
expect(apply).toHaveBeenCalledWith({ nodeCredentials: { Sub1: { credA: 'cred-a' } } });
|
||||
expect(telemetryTrack).toHaveBeenCalledWith(
|
||||
'Instance AI workflow setup step handled',
|
||||
expect.objectContaining({
|
||||
step_kind: 'group',
|
||||
outcome: 'mixed',
|
||||
setup_inputs: [
|
||||
expect.objectContaining({
|
||||
input_type: 'credential',
|
||||
node_type: 'n8n-nodes-base.httpRequest',
|
||||
credential_type: 'credA',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
input_type: 'credential',
|
||||
node_type: 'n8n-nodes-base.httpRequest',
|
||||
credential_type: 'credB',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('reports group step as handled when every member is complete or skipped', () => {
|
||||
const sectionA = makeWorkflowSetupSection({
|
||||
id: 'Sub1:credA',
|
||||
targetNodeName: 'Sub1',
|
||||
credentialType: 'credA',
|
||||
});
|
||||
const sectionB = makeWorkflowSetupSection({
|
||||
id: 'Sub2:credB',
|
||||
targetNodeName: 'Sub2',
|
||||
credentialType: 'credB',
|
||||
});
|
||||
const sections = computed(() => [sectionA, sectionB]);
|
||||
const steps = computed<WorkflowSetupStep[]>(() => [
|
||||
{
|
||||
kind: 'group',
|
||||
group: {
|
||||
subnodeRootNode: { name: 'Agent', type: 'agent', typeVersion: 1, id: 'agent-1' },
|
||||
subnodeSections: [sectionA, sectionB],
|
||||
},
|
||||
},
|
||||
]);
|
||||
const currentStepIndex = ref(0);
|
||||
const isReady = ref(true);
|
||||
const activeStep = computed<WorkflowSetupStep | undefined>(() => steps.value[0]);
|
||||
|
||||
const completedSet = new Set<string>([sectionA.id]);
|
||||
const skippedSet = new Set<string>([sectionB.id]);
|
||||
|
||||
const actions = useWorkflowSetupActions({
|
||||
requestId: ref('req-1'),
|
||||
sections,
|
||||
steps,
|
||||
activeStep,
|
||||
currentStepIndex,
|
||||
isReady,
|
||||
goToStep: vi.fn(),
|
||||
inputs: {
|
||||
credentialSelections: ref({}),
|
||||
isSectionComplete: (s) => completedSet.has(s.id),
|
||||
isSectionSkipped: (s) => skippedSet.has(s.id),
|
||||
markSectionSkipped: vi.fn(),
|
||||
buildCompletedSetupPayload: vi.fn(() => ({})),
|
||||
},
|
||||
applyMachine: { apply: vi.fn(), defer: vi.fn() },
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
thread: { currentThreadId: 't', findToolCallByRequestId: vi.fn() } as any,
|
||||
});
|
||||
|
||||
expect(actions.isStepHandled(steps.value[0])).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
import { computed, ref, type ComputedRef, type Ref } from 'vue';
|
||||
import type { ThreadRuntime } from '../../instanceAi.store';
|
||||
import type {
|
||||
WorkflowSetupApplyPayload,
|
||||
WorkflowSetupSection,
|
||||
WorkflowSetupStep,
|
||||
} from '../workflowSetup.types';
|
||||
import { getStepSections } from '../workflowSetup.helpers';
|
||||
import type { CredentialSelectionsMap } from './useWorkflowSetupInputs';
|
||||
import { useWorkflowSetupTelemetry } from './useWorkflowSetupTelemetry';
|
||||
|
||||
interface WorkflowSetupInputAccessors {
|
||||
credentialSelections: Ref<CredentialSelectionsMap>;
|
||||
isSectionComplete: (section: WorkflowSetupSection) => boolean;
|
||||
isSectionSkipped: (section: WorkflowSetupSection) => boolean;
|
||||
markSectionSkipped: (section: WorkflowSetupSection) => void;
|
||||
buildCompletedSetupPayload: () => WorkflowSetupApplyPayload;
|
||||
}
|
||||
|
||||
interface ApplyMachine {
|
||||
apply: (payload: WorkflowSetupApplyPayload) => Promise<void>;
|
||||
defer: () => Promise<void>;
|
||||
}
|
||||
|
||||
export interface WorkflowSetupActions {
|
||||
nextUnhandledIndex: ComputedRef<number>;
|
||||
hasOtherUnhandledSteps: ComputedRef<boolean>;
|
||||
canAdvanceToNextIncomplete: ComputedRef<boolean>;
|
||||
isStepHandled: (step: WorkflowSetupStep) => boolean;
|
||||
isActionPending: Ref<boolean>;
|
||||
apply: () => Promise<void>;
|
||||
skipCurrentStep: () => Promise<void>;
|
||||
goToNextIncomplete: () => void;
|
||||
}
|
||||
|
||||
export function useWorkflowSetupActions(deps: {
|
||||
requestId: Ref<string>;
|
||||
sections: ComputedRef<WorkflowSetupSection[]>;
|
||||
steps: ComputedRef<WorkflowSetupStep[]>;
|
||||
activeStep: ComputedRef<WorkflowSetupStep | undefined>;
|
||||
currentStepIndex: Ref<number>;
|
||||
isReady: Ref<boolean>;
|
||||
goToStep: (index: number) => void;
|
||||
inputs: WorkflowSetupInputAccessors;
|
||||
applyMachine: ApplyMachine;
|
||||
thread: ThreadRuntime;
|
||||
}): WorkflowSetupActions {
|
||||
const isActionPending = ref(false);
|
||||
const workflowSetupTelemetry = useWorkflowSetupTelemetry({
|
||||
requestId: deps.requestId,
|
||||
sections: deps.sections,
|
||||
steps: deps.steps,
|
||||
activeStep: deps.activeStep,
|
||||
currentStepIndex: deps.currentStepIndex,
|
||||
isReady: deps.isReady,
|
||||
inputs: {
|
||||
isSectionComplete: deps.inputs.isSectionComplete,
|
||||
isSectionSkipped: deps.inputs.isSectionSkipped,
|
||||
},
|
||||
thread: deps.thread,
|
||||
});
|
||||
|
||||
function isStepHandled(step: WorkflowSetupStep): boolean {
|
||||
const sections = getStepSections(step);
|
||||
if (sections.length === 0) return true;
|
||||
return sections.every(
|
||||
(section) => deps.inputs.isSectionComplete(section) || deps.inputs.isSectionSkipped(section),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Globally find the first unhandled step. Prefer indices after the current
|
||||
* step; fall back to indices before. Returns -1 if every step is handled.
|
||||
*/
|
||||
const nextUnhandledIndex = computed(() => {
|
||||
const steps = deps.steps.value;
|
||||
const current = deps.currentStepIndex.value;
|
||||
for (let i = current + 1; i < steps.length; i++) {
|
||||
if (!isStepHandled(steps[i])) return i;
|
||||
}
|
||||
for (let i = 0; i < Math.min(current, steps.length); i++) {
|
||||
if (!isStepHandled(steps[i])) return i;
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
|
||||
const hasOtherUnhandledSteps = computed(() => nextUnhandledIndex.value >= 0);
|
||||
|
||||
const canAdvanceToNextIncomplete = computed(() => {
|
||||
const step = deps.activeStep.value;
|
||||
return step !== undefined && isStepHandled(step) && nextUnhandledIndex.value >= 0;
|
||||
});
|
||||
|
||||
function goToNextIncomplete(): void {
|
||||
if (canAdvanceToNextIncomplete.value) {
|
||||
const step = deps.activeStep.value;
|
||||
if (step) workflowSetupTelemetry.trackStepHandled(step);
|
||||
deps.goToStep(nextUnhandledIndex.value);
|
||||
}
|
||||
}
|
||||
|
||||
async function apply(): Promise<void> {
|
||||
const step = deps.activeStep.value;
|
||||
if (step) workflowSetupTelemetry.trackStepHandled(step);
|
||||
workflowSetupTelemetry.trackSetupInput();
|
||||
await deps.applyMachine.apply(deps.inputs.buildCompletedSetupPayload());
|
||||
}
|
||||
|
||||
async function skipCurrentStep(): Promise<void> {
|
||||
if (isActionPending.value) return;
|
||||
const step = deps.activeStep.value;
|
||||
if (!step) return;
|
||||
|
||||
isActionPending.value = true;
|
||||
try {
|
||||
// Skipping the active step only marks its incomplete sections — already-
|
||||
// complete sections still contribute to the apply payload.
|
||||
const stepSections = getStepSections(step);
|
||||
for (const section of stepSections) {
|
||||
if (!deps.inputs.isSectionComplete(section)) {
|
||||
deps.inputs.markSectionSkipped(section);
|
||||
}
|
||||
}
|
||||
workflowSetupTelemetry.trackStepHandled(step);
|
||||
|
||||
const next = nextUnhandledIndex.value;
|
||||
if (next >= 0) {
|
||||
deps.goToStep(next);
|
||||
return;
|
||||
}
|
||||
|
||||
workflowSetupTelemetry.trackSetupInput();
|
||||
const completedPayload = deps.inputs.buildCompletedSetupPayload();
|
||||
const hasAnyCompleted =
|
||||
Object.keys(completedPayload.nodeCredentials ?? {}).length > 0 ||
|
||||
Object.keys(completedPayload.nodeParameters ?? {}).length > 0;
|
||||
if (hasAnyCompleted) {
|
||||
await deps.applyMachine.apply(completedPayload);
|
||||
} else {
|
||||
await deps.applyMachine.defer();
|
||||
}
|
||||
} finally {
|
||||
isActionPending.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nextUnhandledIndex,
|
||||
hasOtherUnhandledSteps,
|
||||
canAdvanceToNextIncomplete,
|
||||
isStepHandled,
|
||||
isActionPending,
|
||||
apply,
|
||||
skipCurrentStep,
|
||||
goToNextIncomplete,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
import { defineComponent, nextTick, ref, type Ref } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { ThreadRuntime } from '../../instanceAi.store';
|
||||
import { useWorkflowSetupApply } from './useWorkflowSetupApply';
|
||||
|
||||
const toast = vi.hoisted(() => ({
|
||||
showError: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/app/composables/useToast', () => ({
|
||||
useToast: () => toast,
|
||||
}));
|
||||
|
||||
interface Harness {
|
||||
requestId: Ref<string>;
|
||||
result: Ref<unknown>;
|
||||
thread: {
|
||||
confirmAction: ReturnType<typeof vi.fn>;
|
||||
resolveConfirmation: ReturnType<typeof vi.fn>;
|
||||
findToolCallByRequestId: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
applyMachine: ReturnType<typeof useWorkflowSetupApply>;
|
||||
}
|
||||
|
||||
function setupHarness(initialResult?: unknown): Harness {
|
||||
const requestId = ref('req-1');
|
||||
const result = ref<unknown>(initialResult);
|
||||
const thread = {
|
||||
confirmAction: vi.fn().mockResolvedValue(true),
|
||||
resolveConfirmation: vi.fn(),
|
||||
findToolCallByRequestId: vi.fn(() => ({ result: result.value })),
|
||||
};
|
||||
|
||||
const applyMachine = useWorkflowSetupApply({
|
||||
requestId,
|
||||
thread: thread as unknown as ThreadRuntime,
|
||||
});
|
||||
|
||||
return { requestId, result, thread, applyMachine };
|
||||
}
|
||||
|
||||
function setupMountedHarness(initialResult?: unknown): Harness & { unmount: () => void } {
|
||||
let harness!: Harness;
|
||||
const Component = defineComponent({
|
||||
setup() {
|
||||
harness = setupHarness(initialResult);
|
||||
return () => null;
|
||||
},
|
||||
});
|
||||
const wrapper = mount(Component);
|
||||
|
||||
return { ...harness, unmount: () => wrapper.unmount() };
|
||||
}
|
||||
|
||||
describe('useWorkflowSetupApply', () => {
|
||||
beforeEach(() => {
|
||||
toast.showError.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('ignores concurrent apply calls while already applying', async () => {
|
||||
const h = setupHarness();
|
||||
let resolveConfirm: (success: boolean) => void = () => {};
|
||||
h.thread.confirmAction.mockReturnValueOnce(
|
||||
new Promise<boolean>((resolve) => {
|
||||
resolveConfirm = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
const first = h.applyMachine.apply({});
|
||||
await h.applyMachine.apply({});
|
||||
|
||||
expect(h.applyMachine.terminalState.value).toBe('applying');
|
||||
expect(h.thread.confirmAction).toHaveBeenCalledTimes(1);
|
||||
|
||||
resolveConfirm(false);
|
||||
await first;
|
||||
});
|
||||
|
||||
it('posts apply confirmation with node credentials', async () => {
|
||||
const h = setupHarness({ success: true });
|
||||
const nodeCredentials = { 'HTTP Request': { httpBasicAuth: 'cred-1' } };
|
||||
|
||||
await h.applyMachine.apply({ nodeCredentials });
|
||||
|
||||
expect(h.thread.confirmAction).toHaveBeenCalledWith('req-1', {
|
||||
kind: 'setupWorkflowApply',
|
||||
nodeCredentials,
|
||||
});
|
||||
});
|
||||
|
||||
it('posts apply confirmation with node parameters', async () => {
|
||||
const h = setupHarness({ success: true });
|
||||
const nodeParameters = { 'HTTP Request': { url: 'https://example.com/api' } };
|
||||
|
||||
await h.applyMachine.apply({ nodeParameters });
|
||||
|
||||
expect(h.thread.confirmAction).toHaveBeenCalledWith('req-1', {
|
||||
kind: 'setupWorkflowApply',
|
||||
nodeParameters,
|
||||
});
|
||||
});
|
||||
|
||||
it('resets state when posting apply confirmation fails', async () => {
|
||||
const h = setupHarness();
|
||||
h.thread.confirmAction.mockResolvedValueOnce(false);
|
||||
|
||||
await h.applyMachine.apply({});
|
||||
|
||||
expect(h.applyMachine.terminalState.value).toBeNull();
|
||||
expect(toast.showError).not.toHaveBeenCalled();
|
||||
expect(h.thread.resolveConfirmation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets applied terminal state and resolves confirmation on success', async () => {
|
||||
const h = setupHarness({ success: true });
|
||||
|
||||
await h.applyMachine.apply({});
|
||||
|
||||
expect(h.applyMachine.terminalState.value).toBe('applied');
|
||||
expect(h.thread.resolveConfirmation).toHaveBeenCalledWith('req-1', 'approved');
|
||||
});
|
||||
|
||||
it('sets partial terminal state when the result is partial', async () => {
|
||||
const h = setupHarness({ success: true, partial: true });
|
||||
|
||||
await h.applyMachine.apply({});
|
||||
|
||||
expect(h.applyMachine.terminalState.value).toBe('partial');
|
||||
expect(h.thread.resolveConfirmation).toHaveBeenCalledWith('req-1', 'approved');
|
||||
});
|
||||
|
||||
it('shows an error and resets state when apply result fails', async () => {
|
||||
const h = setupHarness({ success: false, error: 'Could not apply credentials' });
|
||||
|
||||
await h.applyMachine.apply({});
|
||||
|
||||
expect(h.applyMachine.terminalState.value).toBeNull();
|
||||
expect(toast.showError).toHaveBeenCalledWith(expect.any(Error), 'Setup failed');
|
||||
expect(toast.showError.mock.calls[0][0].message).toBe('Could not apply credentials');
|
||||
expect(h.thread.resolveConfirmation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses the latest request id when applying', async () => {
|
||||
const h = setupHarness({ success: true });
|
||||
h.requestId.value = 'req-2';
|
||||
|
||||
await h.applyMachine.apply({});
|
||||
|
||||
expect(h.thread.confirmAction).toHaveBeenCalledWith(
|
||||
'req-2',
|
||||
expect.objectContaining({ kind: 'setupWorkflowApply' }),
|
||||
);
|
||||
expect(h.thread.resolveConfirmation).toHaveBeenCalledWith('req-2', 'approved');
|
||||
});
|
||||
|
||||
it('waits for an async tool result after posting apply confirmation', async () => {
|
||||
const h = setupHarness();
|
||||
|
||||
const pendingApply = h.applyMachine.apply({});
|
||||
await nextTick();
|
||||
expect(h.applyMachine.terminalState.value).toBe('applying');
|
||||
|
||||
h.result.value = { success: true };
|
||||
await pendingApply;
|
||||
|
||||
expect(h.applyMachine.terminalState.value).toBe('applied');
|
||||
});
|
||||
|
||||
it('times out waiting for an apply result', async () => {
|
||||
vi.useFakeTimers();
|
||||
const h = setupHarness();
|
||||
|
||||
const pendingApply = h.applyMachine.apply({});
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
await pendingApply;
|
||||
|
||||
expect(h.applyMachine.terminalState.value).toBeNull();
|
||||
expect(toast.showError).toHaveBeenCalledWith(expect.any(Error), 'Setup failed');
|
||||
expect(toast.showError.mock.calls[0][0].message).toBe('Apply timed out — please try again.');
|
||||
});
|
||||
|
||||
it('settles a pending apply when unmounted', async () => {
|
||||
const h = setupMountedHarness();
|
||||
|
||||
const pendingApply = h.applyMachine.apply({});
|
||||
await nextTick();
|
||||
expect(h.applyMachine.terminalState.value).toBe('applying');
|
||||
|
||||
h.unmount();
|
||||
await pendingApply;
|
||||
|
||||
expect(h.applyMachine.terminalState.value).toBeNull();
|
||||
expect(toast.showError).not.toHaveBeenCalled();
|
||||
expect(h.thread.resolveConfirmation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ignores defer calls while already applying', async () => {
|
||||
const h = setupHarness();
|
||||
let resolveConfirm: (success: boolean) => void = () => {};
|
||||
h.thread.confirmAction.mockReturnValueOnce(
|
||||
new Promise<boolean>((resolve) => {
|
||||
resolveConfirm = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
const first = h.applyMachine.defer();
|
||||
await h.applyMachine.defer();
|
||||
|
||||
expect(h.applyMachine.terminalState.value).toBe('applying');
|
||||
expect(h.thread.confirmAction).toHaveBeenCalledTimes(1);
|
||||
|
||||
resolveConfirm(false);
|
||||
await first;
|
||||
});
|
||||
|
||||
it('sets deferred terminal state and resolves confirmation on defer success', async () => {
|
||||
const h = setupHarness();
|
||||
|
||||
await h.applyMachine.defer();
|
||||
|
||||
expect(h.thread.confirmAction).toHaveBeenCalledWith('req-1', {
|
||||
kind: 'approval',
|
||||
approved: false,
|
||||
});
|
||||
expect(h.applyMachine.terminalState.value).toBe('deferred');
|
||||
expect(h.thread.resolveConfirmation).toHaveBeenCalledWith('req-1', 'deferred');
|
||||
});
|
||||
|
||||
it('resets state when defer confirmation fails', async () => {
|
||||
const h = setupHarness();
|
||||
h.thread.confirmAction.mockResolvedValueOnce(false);
|
||||
|
||||
await h.applyMachine.defer();
|
||||
|
||||
expect(h.applyMachine.terminalState.value).toBeNull();
|
||||
expect(h.thread.resolveConfirmation).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
import { ref, watch, onUnmounted, type Ref } from 'vue';
|
||||
import type { InstanceAiToolCallState } from '@n8n/api-types';
|
||||
import { useToast } from '@/app/composables/useToast';
|
||||
import type { ThreadRuntime } from '../../instanceAi.store';
|
||||
import type { TerminalState, WorkflowSetupApplyPayload } from '../workflowSetup.types';
|
||||
|
||||
const APPLY_TIMEOUT_MS = 60_000;
|
||||
const WAIT_CANCELLED = Symbol('wait-cancelled');
|
||||
|
||||
type WaitForToolResult = Record<string, unknown> | null | typeof WAIT_CANCELLED;
|
||||
|
||||
export function useWorkflowSetupApply(deps: {
|
||||
requestId: Ref<string>;
|
||||
thread: ThreadRuntime;
|
||||
}): {
|
||||
terminalState: Ref<TerminalState | null>;
|
||||
apply: (payload: WorkflowSetupApplyPayload) => Promise<void>;
|
||||
defer: () => Promise<void>;
|
||||
} {
|
||||
const toast = useToast();
|
||||
|
||||
const terminalState = ref<TerminalState | null>(null);
|
||||
|
||||
let cancelWait: (() => void) | null = null;
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelWait?.();
|
||||
});
|
||||
|
||||
function isToolResult(val: unknown): val is Record<string, unknown> {
|
||||
return typeof val === 'object' && val !== null && !Array.isArray(val);
|
||||
}
|
||||
|
||||
function waitForToolResult(requestId: string): {
|
||||
promise: Promise<WaitForToolResult>;
|
||||
cancel: () => void;
|
||||
} {
|
||||
let stopWatch: (() => void) | null = null;
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
let resolveWait: ((result: WaitForToolResult) => void) | null = null;
|
||||
|
||||
function cleanup() {
|
||||
if (stopWatch) {
|
||||
stopWatch();
|
||||
stopWatch = null;
|
||||
}
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function finish(result: WaitForToolResult) {
|
||||
cleanup();
|
||||
resolveWait?.(result);
|
||||
resolveWait = null;
|
||||
}
|
||||
|
||||
const promise = new Promise<WaitForToolResult>((resolve) => {
|
||||
resolveWait = resolve;
|
||||
const existing = deps.thread.findToolCallByRequestId(requestId);
|
||||
if (existing?.result !== undefined) {
|
||||
finish(isToolResult(existing.result) ? existing.result : null);
|
||||
return;
|
||||
}
|
||||
|
||||
stopWatch = watch(
|
||||
() => {
|
||||
const tc: InstanceAiToolCallState | undefined =
|
||||
deps.thread.findToolCallByRequestId(requestId);
|
||||
return tc?.result;
|
||||
},
|
||||
(result) => {
|
||||
if (result !== undefined) {
|
||||
finish(isToolResult(result) ? result : null);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
finish(null);
|
||||
}, APPLY_TIMEOUT_MS);
|
||||
});
|
||||
|
||||
return { promise, cancel: () => finish(WAIT_CANCELLED) };
|
||||
}
|
||||
|
||||
async function apply(payload: WorkflowSetupApplyPayload): Promise<void> {
|
||||
if (terminalState.value === 'applying') return;
|
||||
terminalState.value = 'applying';
|
||||
|
||||
const postSuccess = await deps.thread.confirmAction(deps.requestId.value, {
|
||||
kind: 'setupWorkflowApply',
|
||||
...payload,
|
||||
});
|
||||
|
||||
// confirmAction already toasts on POST failure; just reset so the wizard
|
||||
// re-renders with the user's selections and they can try again.
|
||||
if (!postSuccess) {
|
||||
terminalState.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
cancelWait?.();
|
||||
const { promise, cancel } = waitForToolResult(deps.requestId.value);
|
||||
cancelWait = cancel;
|
||||
const result = await promise;
|
||||
cancelWait = null;
|
||||
if (result === WAIT_CANCELLED) {
|
||||
terminalState.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (result === null) {
|
||||
toast.showError(new Error('Apply timed out — please try again.'), 'Setup failed');
|
||||
terminalState.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.success === true) {
|
||||
terminalState.value = result.partial === true ? 'partial' : 'applied';
|
||||
deps.thread.resolveConfirmation(deps.requestId.value, 'approved');
|
||||
return;
|
||||
}
|
||||
|
||||
const message = typeof result.error === 'string' ? result.error : 'Apply failed.';
|
||||
toast.showError(new Error(message), 'Setup failed');
|
||||
terminalState.value = null;
|
||||
}
|
||||
|
||||
async function defer(): Promise<void> {
|
||||
if (terminalState.value === 'applying') return;
|
||||
terminalState.value = 'applying';
|
||||
|
||||
const success = await deps.thread.confirmAction(deps.requestId.value, {
|
||||
kind: 'approval',
|
||||
approved: false,
|
||||
});
|
||||
if (success) {
|
||||
terminalState.value = 'deferred';
|
||||
deps.thread.resolveConfirmation(deps.requestId.value, 'deferred');
|
||||
return;
|
||||
}
|
||||
// confirmAction already toasted. Reset so the wizard re-renders.
|
||||
terminalState.value = null;
|
||||
}
|
||||
|
||||
return {
|
||||
terminalState,
|
||||
apply,
|
||||
defer,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import { ref } from 'vue';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
import { useCredentialsStore } from '@/features/credentials/credentials.store';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
|
||||
import { useWorkflowSetupBootstrap } from './useWorkflowSetupBootstrap';
|
||||
|
||||
describe('useWorkflowSetupBootstrap', () => {
|
||||
let credentialsStore: ReturnType<typeof mockedStore<typeof useCredentialsStore>>;
|
||||
let nodeTypesStore: ReturnType<typeof mockedStore<typeof useNodeTypesStore>>;
|
||||
|
||||
beforeEach(() => {
|
||||
createTestingPinia();
|
||||
credentialsStore = mockedStore(useCredentialsStore);
|
||||
nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||
|
||||
credentialsStore.fetchAllCredentialsForWorkflow = vi.fn().mockResolvedValue([]);
|
||||
credentialsStore.fetchCredentialTypes = vi.fn().mockResolvedValue(undefined);
|
||||
nodeTypesStore.loadNodeTypesIfNotLoaded = vi.fn().mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('throws when workflowId is missing', async () => {
|
||||
const { bootstrap } = useWorkflowSetupBootstrap(ref(undefined));
|
||||
|
||||
await expect(bootstrap()).rejects.toThrow('useWorkflowSetupBootstrap: workflowId is required');
|
||||
expect(credentialsStore.fetchAllCredentialsForWorkflow).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fetches workflow-scoped credentials and flips isReady after success', async () => {
|
||||
const { isReady, bootstrap } = useWorkflowSetupBootstrap(ref('workflow-1'));
|
||||
|
||||
expect(isReady.value).toBe(false);
|
||||
|
||||
await bootstrap();
|
||||
|
||||
expect(credentialsStore.fetchAllCredentialsForWorkflow).toHaveBeenCalledWith({
|
||||
workflowId: 'workflow-1',
|
||||
});
|
||||
expect(credentialsStore.fetchCredentialTypes).toHaveBeenCalled();
|
||||
expect(nodeTypesStore.loadNodeTypesIfNotLoaded).toHaveBeenCalled();
|
||||
expect(isReady.value).toBe(true);
|
||||
});
|
||||
|
||||
it('flips isReady to true even when one of the parallel calls fails', async () => {
|
||||
credentialsStore.fetchAllCredentialsForWorkflow = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('network'));
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
const { isReady, bootstrap } = useWorkflowSetupBootstrap(ref('workflow-1'));
|
||||
|
||||
await bootstrap();
|
||||
|
||||
expect(isReady.value).toBe(true);
|
||||
expect(warnSpy).toHaveBeenCalled();
|
||||
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('uses the latest projectId from the ref at the time bootstrap is called', async () => {
|
||||
const workflowId = ref<string | undefined>('workflow-1');
|
||||
const { bootstrap } = useWorkflowSetupBootstrap(workflowId);
|
||||
|
||||
workflowId.value = 'workflow-2';
|
||||
await bootstrap();
|
||||
|
||||
expect(credentialsStore.fetchAllCredentialsForWorkflow).toHaveBeenCalledWith({
|
||||
workflowId: 'workflow-2',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { ref, type Ref } from 'vue';
|
||||
import { useCredentialsStore } from '@/features/credentials/credentials.store';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
|
||||
export function useWorkflowSetupBootstrap(workflowId: Ref<string | undefined>): {
|
||||
isReady: Ref<boolean>;
|
||||
bootstrap: () => Promise<void>;
|
||||
} {
|
||||
const credentialsStore = useCredentialsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
|
||||
const isReady = ref(false);
|
||||
|
||||
async function bootstrap() {
|
||||
isReady.value = false;
|
||||
const wid = workflowId.value;
|
||||
if (!wid) {
|
||||
throw new Error('useWorkflowSetupBootstrap: workflowId is required');
|
||||
}
|
||||
try {
|
||||
await Promise.all([
|
||||
credentialsStore.fetchAllCredentialsForWorkflow({ workflowId: wid }),
|
||||
credentialsStore.fetchCredentialTypes(false),
|
||||
nodeTypesStore.loadNodeTypesIfNotLoaded(),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.warn('[InstanceAI] Workflow setup bootstrap partial failure', error);
|
||||
} finally {
|
||||
isReady.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
return { isReady, bootstrap };
|
||||
}
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
import {
|
||||
computed,
|
||||
inject,
|
||||
onMounted,
|
||||
provide,
|
||||
ref,
|
||||
watch,
|
||||
type ComputedRef,
|
||||
type InjectionKey,
|
||||
type Ref,
|
||||
} from 'vue';
|
||||
import type { InstanceAiCredentialFlow, InstanceAiWorkflowSetupNode } from '@n8n/api-types';
|
||||
import { useCredentialTestInBackground } from '@/features/credentials/composables/useCredentialTestInBackground';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import { useThread } from '../../instanceAi.store';
|
||||
import type {
|
||||
TerminalState,
|
||||
WorkflowSetupSection,
|
||||
WorkflowSetupStep,
|
||||
} from '../workflowSetup.types';
|
||||
import { getStepSections } from '../workflowSetup.helpers';
|
||||
import { useWorkflowSetupActions } from './useWorkflowSetupActions';
|
||||
import { useWorkflowSetupApply } from './useWorkflowSetupApply';
|
||||
import { useWorkflowSetupBootstrap } from './useWorkflowSetupBootstrap';
|
||||
import { useWorkflowSetupSections } from './useWorkflowSetupSections';
|
||||
import { useWorkflowSetupSteps } from './useWorkflowSetupSteps';
|
||||
import { useWorkflowSetupInputs, type CredentialSelectionsMap } from './useWorkflowSetupInputs';
|
||||
|
||||
export interface WorkflowSetupContext {
|
||||
sections: ComputedRef<WorkflowSetupSection[]>;
|
||||
steps: ComputedRef<WorkflowSetupStep[]>;
|
||||
currentStepIndex: Ref<number>;
|
||||
activeStep: ComputedRef<WorkflowSetupStep | undefined>;
|
||||
hasOtherUnhandledSteps: ComputedRef<boolean>;
|
||||
canAdvanceToNextIncomplete: ComputedRef<boolean>;
|
||||
credentialSelections: Ref<CredentialSelectionsMap>;
|
||||
terminalState: Ref<TerminalState | null>;
|
||||
isReady: Ref<boolean>;
|
||||
projectId: ComputedRef<string | undefined>;
|
||||
credentialFlow: ComputedRef<InstanceAiCredentialFlow | undefined>;
|
||||
isActionPending: Ref<boolean>;
|
||||
setCredential: (section: WorkflowSetupSection, credId: string | null) => void;
|
||||
setParameterValue: (section: WorkflowSetupSection, parameterName: string, value: unknown) => void;
|
||||
getDisplayNode: (section: WorkflowSetupSection) => INodeUi;
|
||||
isSectionComplete: (section: WorkflowSetupSection) => boolean;
|
||||
isCredentialTestFailed: (section: WorkflowSetupSection) => boolean;
|
||||
isSectionSkipped: (section: WorkflowSetupSection) => boolean;
|
||||
isStepComplete: (step: WorkflowSetupStep) => boolean;
|
||||
isStepSkipped: (step: WorkflowSetupStep) => boolean;
|
||||
isStepHandled: (step: WorkflowSetupStep) => boolean;
|
||||
goToStep: (index: number) => void;
|
||||
goToNext: () => void;
|
||||
goToPrev: () => void;
|
||||
goToNextIncomplete: () => void;
|
||||
apply: () => Promise<void>;
|
||||
skipCurrentStep: () => Promise<void>;
|
||||
}
|
||||
|
||||
const WorkflowSetupContextKey: InjectionKey<WorkflowSetupContext> = Symbol('WorkflowSetupContext');
|
||||
|
||||
interface ProvideOptions {
|
||||
requestId: Ref<string>;
|
||||
setupRequests: Ref<InstanceAiWorkflowSetupNode[]>;
|
||||
projectId: Ref<string | undefined>;
|
||||
workflowId: Ref<string | undefined>;
|
||||
credentialFlow: Ref<InstanceAiCredentialFlow | undefined>;
|
||||
}
|
||||
|
||||
export function provideWorkflowSetupContext(opts: ProvideOptions): WorkflowSetupContext {
|
||||
const thread = useThread();
|
||||
const { hydrateCredentialTestResults } = useCredentialTestInBackground();
|
||||
|
||||
hydrateCredentialTestResults(
|
||||
opts.setupRequests.value.flatMap((req) => {
|
||||
const credType = req.credentialType;
|
||||
const credId = credType ? req.node.credentials?.[credType]?.id : undefined;
|
||||
const result = req.credentialTestResult;
|
||||
return credId && result ? [{ id: credId, success: result.success }] : [];
|
||||
}),
|
||||
);
|
||||
|
||||
const { sections } = useWorkflowSetupSections(opts.setupRequests);
|
||||
const { steps } = useWorkflowSetupSteps({ sections, setupRequests: opts.setupRequests });
|
||||
const bootstrap = useWorkflowSetupBootstrap(opts.workflowId);
|
||||
const applyMachine = useWorkflowSetupApply({
|
||||
requestId: opts.requestId,
|
||||
thread,
|
||||
});
|
||||
|
||||
const currentStepIndex = ref(0);
|
||||
const activeStep = computed(() => steps.value[currentStepIndex.value]);
|
||||
|
||||
const inputsState = useWorkflowSetupInputs({ sections });
|
||||
|
||||
const projectId = computed(() => opts.projectId.value);
|
||||
const credentialFlow = computed(() => opts.credentialFlow.value);
|
||||
|
||||
function goToStep(index: number) {
|
||||
if (index >= 0 && index < steps.value.length) {
|
||||
currentStepIndex.value = index;
|
||||
}
|
||||
}
|
||||
|
||||
function goToNext() {
|
||||
if (currentStepIndex.value < steps.value.length - 1) {
|
||||
currentStepIndex.value++;
|
||||
}
|
||||
}
|
||||
|
||||
function goToPrev() {
|
||||
if (currentStepIndex.value > 0) {
|
||||
currentStepIndex.value--;
|
||||
}
|
||||
}
|
||||
|
||||
const actions = useWorkflowSetupActions({
|
||||
requestId: opts.requestId,
|
||||
sections,
|
||||
steps,
|
||||
activeStep,
|
||||
currentStepIndex,
|
||||
isReady: bootstrap.isReady,
|
||||
goToStep,
|
||||
inputs: {
|
||||
credentialSelections: inputsState.credentialSelections,
|
||||
isSectionComplete: inputsState.isSectionComplete,
|
||||
isSectionSkipped: inputsState.isSectionSkipped,
|
||||
markSectionSkipped: inputsState.markSectionSkipped,
|
||||
buildCompletedSetupPayload: inputsState.buildCompletedSetupPayload,
|
||||
},
|
||||
applyMachine: {
|
||||
apply: applyMachine.apply,
|
||||
defer: applyMachine.defer,
|
||||
},
|
||||
thread,
|
||||
});
|
||||
|
||||
function isStepComplete(step: WorkflowSetupStep): boolean {
|
||||
const stepSections = getStepSections(step);
|
||||
if (stepSections.length === 0) return false;
|
||||
return stepSections.every(inputsState.isSectionComplete);
|
||||
}
|
||||
|
||||
function isStepSkipped(step: WorkflowSetupStep): boolean {
|
||||
const stepSections = getStepSections(step);
|
||||
if (stepSections.length === 0) return false;
|
||||
return stepSections.every(inputsState.isSectionSkipped);
|
||||
}
|
||||
|
||||
// Clamp currentStepIndex when the step list shrinks beneath it.
|
||||
watch(
|
||||
() => steps.value.length,
|
||||
(len) => {
|
||||
if (currentStepIndex.value >= len) {
|
||||
currentStepIndex.value = Math.max(0, len - 1);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await bootstrap.bootstrap();
|
||||
});
|
||||
|
||||
const context: WorkflowSetupContext = {
|
||||
sections,
|
||||
steps,
|
||||
currentStepIndex,
|
||||
activeStep,
|
||||
hasOtherUnhandledSteps: actions.hasOtherUnhandledSteps,
|
||||
canAdvanceToNextIncomplete: actions.canAdvanceToNextIncomplete,
|
||||
credentialSelections: inputsState.credentialSelections,
|
||||
terminalState: applyMachine.terminalState,
|
||||
isReady: bootstrap.isReady,
|
||||
projectId,
|
||||
credentialFlow,
|
||||
isActionPending: actions.isActionPending,
|
||||
setCredential: inputsState.setCredential,
|
||||
setParameterValue: inputsState.setParameterValue,
|
||||
getDisplayNode: inputsState.getDisplayNode,
|
||||
isSectionComplete: inputsState.isSectionComplete,
|
||||
isCredentialTestFailed: inputsState.isCredentialTestFailed,
|
||||
isSectionSkipped: inputsState.isSectionSkipped,
|
||||
isStepComplete,
|
||||
isStepSkipped,
|
||||
isStepHandled: actions.isStepHandled,
|
||||
goToStep,
|
||||
goToNext,
|
||||
goToPrev,
|
||||
goToNextIncomplete: actions.goToNextIncomplete,
|
||||
apply: actions.apply,
|
||||
skipCurrentStep: actions.skipCurrentStep,
|
||||
};
|
||||
|
||||
provide(WorkflowSetupContextKey, context);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useWorkflowSetupContext(): WorkflowSetupContext {
|
||||
const ctx = inject(WorkflowSetupContextKey);
|
||||
if (!ctx) {
|
||||
throw new Error(
|
||||
'useWorkflowSetupContext: called outside of a provideWorkflowSetupContext scope',
|
||||
);
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import { computed, reactive, watch, type Ref } from 'vue';
|
||||
import type { WorkflowSetupGroup, WorkflowSetupSection } from '../workflowSetup.types';
|
||||
import { getGroupSections } from '../workflowSetup.helpers';
|
||||
import { useWorkflowSetupContext } from './useWorkflowSetupContext';
|
||||
|
||||
/**
|
||||
* Manages per-section expansion state inside a `WorkflowSetupGroup`.
|
||||
*
|
||||
* Only sub-node sections are collapsible: the root section renders inline
|
||||
* as the group card's primary body and is always visible.
|
||||
*
|
||||
* Sections that contain parameters stay open so the user isn't interrupted
|
||||
* mid-typing — only credential-only sections auto-collapse on completion.
|
||||
*/
|
||||
export function useWorkflowSetupGroupSections(group: Ref<WorkflowSetupGroup>) {
|
||||
const ctx = useWorkflowSetupContext();
|
||||
|
||||
const allSections = computed<WorkflowSetupSection[]>(() => getGroupSections(group.value));
|
||||
|
||||
const expandedSections = reactive<Record<string, boolean>>({});
|
||||
|
||||
function initExpandState() {
|
||||
for (const section of group.value.subnodeSections) {
|
||||
if (!(section.id in expandedSections)) {
|
||||
expandedSections[section.id] = false;
|
||||
}
|
||||
}
|
||||
const firstIncomplete = group.value.subnodeSections.find(
|
||||
(section) => !ctx.isSectionComplete(section),
|
||||
);
|
||||
if (firstIncomplete) {
|
||||
expandedSections[firstIncomplete.id] = true;
|
||||
}
|
||||
}
|
||||
initExpandState();
|
||||
|
||||
function toggleSection(sectionId: string) {
|
||||
expandedSections[sectionId] = !expandedSections[sectionId];
|
||||
}
|
||||
|
||||
watch(
|
||||
() => allSections.value.map((section) => [section.id, ctx.isSectionComplete(section)] as const),
|
||||
(states, prevStates) => {
|
||||
const prev = new Map(prevStates ?? []);
|
||||
for (const [sectionId, isComplete] of states) {
|
||||
if (!isComplete || prev.get(sectionId)) continue;
|
||||
const section = allSections.value.find((s) => s.id === sectionId);
|
||||
if (!section || section.parameterNames.length > 0) continue;
|
||||
if (sectionId in expandedSections) {
|
||||
expandedSections[sectionId] = false;
|
||||
}
|
||||
const nextIncomplete = group.value.subnodeSections.find(
|
||||
(s) => !ctx.isSectionComplete(s) && s.id !== sectionId,
|
||||
);
|
||||
if (nextIncomplete) {
|
||||
expandedSections[nextIncomplete.id] = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
return {
|
||||
expandedSections,
|
||||
toggleSection,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,488 @@
|
|||
import { computed, nextTick, ref, type ComputedRef, type Ref } from 'vue';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { makeWorkflowSetupSection } from '../__tests__/factories';
|
||||
import type { WorkflowSetupSection } from '../workflowSetup.types';
|
||||
import { useWorkflowSetupInputs } from './useWorkflowSetupInputs';
|
||||
|
||||
interface TestCredential {
|
||||
id: string;
|
||||
type: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const credentialsStore = vi.hoisted(() => ({
|
||||
credentials: new Map<string, TestCredential>(),
|
||||
credentialTestResults: new Map<string, string>(),
|
||||
getCredentialById: vi.fn((id: string) => credentialsStore.credentials.get(id)),
|
||||
isCredentialTestedOk: vi.fn(
|
||||
(id: string) => credentialsStore.credentialTestResults.get(id) === 'success',
|
||||
),
|
||||
}));
|
||||
|
||||
const credentialTest = vi.hoisted(() => ({
|
||||
testableTypes: new Set<string>(),
|
||||
isCredentialTypeTestable: vi.fn((type: string) => credentialTest.testableTypes.has(type)),
|
||||
testCredentialInBackground: vi.fn(),
|
||||
}));
|
||||
|
||||
const nodeTypesStore = vi.hoisted(() => ({
|
||||
getNodeType: vi.fn((): unknown => null),
|
||||
}));
|
||||
|
||||
vi.mock('@/features/credentials/credentials.store', () => ({
|
||||
useCredentialsStore: () => credentialsStore,
|
||||
}));
|
||||
|
||||
vi.mock('@/features/credentials/composables/useCredentialTestInBackground', () => ({
|
||||
useCredentialTestInBackground: () => ({
|
||||
isCredentialTypeTestable: credentialTest.isCredentialTypeTestable,
|
||||
testCredentialInBackground: credentialTest.testCredentialInBackground,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/app/stores/nodeTypes.store', () => ({
|
||||
useNodeTypesStore: () => nodeTypesStore,
|
||||
}));
|
||||
|
||||
interface Harness {
|
||||
sectionA: WorkflowSetupSection;
|
||||
sectionB: WorkflowSetupSection;
|
||||
sectionsRef: Ref<WorkflowSetupSection[]>;
|
||||
sections: ComputedRef<WorkflowSetupSection[]>;
|
||||
inputs: ReturnType<typeof useWorkflowSetupInputs>;
|
||||
}
|
||||
|
||||
function addCredential(credential: TestCredential): void {
|
||||
credentialsStore.credentials.set(credential.id, credential);
|
||||
}
|
||||
|
||||
function setupHarness(sections?: WorkflowSetupSection[]): Harness {
|
||||
const sectionA = makeWorkflowSetupSection({
|
||||
id: 'HTTP Request:httpBasicAuth',
|
||||
targetNodeName: 'HTTP Request',
|
||||
credentialType: 'httpBasicAuth',
|
||||
});
|
||||
const sectionB = makeWorkflowSetupSection({
|
||||
id: 'Slack:slackApi',
|
||||
targetNodeName: 'Slack',
|
||||
credentialType: 'slackApi',
|
||||
});
|
||||
const sectionsRef = ref(sections ?? [sectionA, sectionB]);
|
||||
const sectionsComputed = computed(() => sectionsRef.value);
|
||||
|
||||
const inputs = useWorkflowSetupInputs({
|
||||
sections: sectionsComputed,
|
||||
});
|
||||
|
||||
return {
|
||||
sectionA,
|
||||
sectionB,
|
||||
sectionsRef,
|
||||
sections: sectionsComputed,
|
||||
inputs,
|
||||
};
|
||||
}
|
||||
|
||||
describe('useWorkflowSetupInputs', () => {
|
||||
beforeEach(() => {
|
||||
credentialsStore.credentials.clear();
|
||||
credentialsStore.credentialTestResults.clear();
|
||||
credentialsStore.getCredentialById.mockClear();
|
||||
credentialsStore.isCredentialTestedOk.mockClear();
|
||||
credentialTest.testableTypes.clear();
|
||||
credentialTest.isCredentialTypeTestable.mockClear();
|
||||
credentialTest.testCredentialInBackground.mockClear();
|
||||
nodeTypesStore.getNodeType.mockReset();
|
||||
nodeTypesStore.getNodeType.mockReturnValue(null);
|
||||
});
|
||||
|
||||
it('sets a selection, tests it in the background, and clears a previous skip', () => {
|
||||
addCredential({ id: 'cred-1', type: 'httpBasicAuth', name: 'HTTP credential' });
|
||||
const h = setupHarness();
|
||||
h.inputs.markSectionSkipped(h.sectionA);
|
||||
|
||||
h.inputs.setCredential(h.sectionA, 'cred-1');
|
||||
|
||||
expect(h.inputs.credentialSelections.value).toEqual({
|
||||
'HTTP Request': { httpBasicAuth: 'cred-1' },
|
||||
});
|
||||
expect(credentialTest.testCredentialInBackground).toHaveBeenCalledWith(
|
||||
'cred-1',
|
||||
'HTTP credential',
|
||||
'httpBasicAuth',
|
||||
);
|
||||
expect(h.inputs.isSectionSkipped(h.sectionA)).toBe(false);
|
||||
});
|
||||
|
||||
it('removes a selected credential when set to null', () => {
|
||||
const h = setupHarness();
|
||||
h.inputs.setCredential(h.sectionA, 'cred-1');
|
||||
|
||||
h.inputs.setCredential(h.sectionA, null);
|
||||
|
||||
expect(h.inputs.credentialSelections.value).toEqual({ 'HTTP Request': {} });
|
||||
expect(h.inputs.buildCompletedSetupPayload()).toEqual({});
|
||||
});
|
||||
|
||||
it('reports section completion based on testability and credential test result', () => {
|
||||
const h = setupHarness();
|
||||
|
||||
expect(h.inputs.isSectionComplete(h.sectionA)).toBe(false);
|
||||
|
||||
h.inputs.setCredential(h.sectionA, 'cred-1');
|
||||
expect(h.inputs.isSectionComplete(h.sectionA)).toBe(true);
|
||||
|
||||
credentialTest.testableTypes.add('httpBasicAuth');
|
||||
expect(h.inputs.isSectionComplete(h.sectionA)).toBe(false);
|
||||
|
||||
credentialsStore.credentialTestResults.set('cred-1', 'success');
|
||||
expect(h.inputs.isSectionComplete(h.sectionA)).toBe(true);
|
||||
});
|
||||
|
||||
it('reports credential test failures only for selected testable credentials', () => {
|
||||
const h = setupHarness();
|
||||
h.inputs.setCredential(h.sectionA, 'cred-1');
|
||||
credentialsStore.credentialTestResults.set('cred-1', 'error');
|
||||
|
||||
expect(h.inputs.isCredentialTestFailed(h.sectionA)).toBe(false);
|
||||
|
||||
credentialTest.testableTypes.add('httpBasicAuth');
|
||||
expect(h.inputs.isCredentialTestFailed(h.sectionA)).toBe(true);
|
||||
expect(h.inputs.isCredentialTestFailed(h.sectionB)).toBe(false);
|
||||
});
|
||||
|
||||
it('marks skipped sections idempotently and clears them when input is provided', () => {
|
||||
const h = setupHarness();
|
||||
|
||||
h.inputs.markSectionSkipped(h.sectionA);
|
||||
h.inputs.markSectionSkipped(h.sectionA);
|
||||
|
||||
expect(h.inputs.isSectionSkipped(h.sectionA)).toBe(true);
|
||||
expect(h.inputs.skippedSectionIds.value).toEqual(new Set([h.sectionA.id]));
|
||||
|
||||
h.inputs.setCredential(h.sectionA, 'cred-1');
|
||||
|
||||
expect(h.inputs.isSectionSkipped(h.sectionA)).toBe(false);
|
||||
expect(h.inputs.skippedSectionIds.value).toEqual(new Set());
|
||||
});
|
||||
|
||||
it('builds only completed selections into the payload', () => {
|
||||
const h = setupHarness();
|
||||
credentialTest.testableTypes.add('httpBasicAuth');
|
||||
h.inputs.setCredential(h.sectionA, 'cred-1');
|
||||
h.inputs.setCredential(h.sectionB, 'cred-2');
|
||||
|
||||
expect(h.inputs.buildCompletedSetupPayload()).toEqual({
|
||||
nodeCredentials: {
|
||||
Slack: { slackApi: 'cred-2' },
|
||||
},
|
||||
});
|
||||
|
||||
credentialsStore.credentialTestResults.set('cred-1', 'success');
|
||||
|
||||
expect(h.inputs.buildCompletedSetupPayload()).toEqual({
|
||||
nodeCredentials: {
|
||||
'HTTP Request': { httpBasicAuth: 'cred-1' },
|
||||
Slack: { slackApi: 'cred-2' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('seeds selections from current credentials and tests seeded credentials', async () => {
|
||||
addCredential({ id: 'current-cred', type: 'httpBasicAuth', name: 'Current credential' });
|
||||
const section = makeWorkflowSetupSection({
|
||||
targetNodeName: 'HTTP Request',
|
||||
credentialType: 'httpBasicAuth',
|
||||
currentCredentialId: 'current-cred',
|
||||
});
|
||||
|
||||
const h = setupHarness([section]);
|
||||
await nextTick();
|
||||
|
||||
expect(h.inputs.credentialSelections.value).toEqual({
|
||||
'HTTP Request': { httpBasicAuth: 'current-cred' },
|
||||
});
|
||||
expect(credentialTest.testCredentialInBackground).toHaveBeenCalledWith(
|
||||
'current-cred',
|
||||
'Current credential',
|
||||
'httpBasicAuth',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not overwrite an existing user selection when sections refresh', async () => {
|
||||
const h = setupHarness();
|
||||
h.inputs.setCredential(h.sectionA, 'user-cred');
|
||||
|
||||
h.sectionsRef.value = [
|
||||
{
|
||||
...h.sectionA,
|
||||
currentCredentialId: 'refreshed-current-cred',
|
||||
},
|
||||
];
|
||||
await nextTick();
|
||||
|
||||
expect(h.inputs.credentialSelections.value).toEqual({
|
||||
'HTTP Request': { httpBasicAuth: 'user-cred' },
|
||||
});
|
||||
});
|
||||
|
||||
it('does not re-seed a credential after the user explicitly clears it', async () => {
|
||||
addCredential({ id: 'current-cred', type: 'httpBasicAuth', name: 'Current credential' });
|
||||
const section = makeWorkflowSetupSection({
|
||||
targetNodeName: 'HTTP Request',
|
||||
credentialType: 'httpBasicAuth',
|
||||
currentCredentialId: 'current-cred',
|
||||
});
|
||||
|
||||
const h = setupHarness([section]);
|
||||
await nextTick();
|
||||
|
||||
expect(h.inputs.credentialSelections.value).toEqual({
|
||||
'HTTP Request': { httpBasicAuth: 'current-cred' },
|
||||
});
|
||||
|
||||
h.inputs.setCredential(section, null);
|
||||
expect(h.inputs.credentialSelections.value).toEqual({ 'HTTP Request': {} });
|
||||
|
||||
// Sections re-emit with the same content (e.g. due to upstream reactive recomputation).
|
||||
h.sectionsRef.value = [...h.sectionsRef.value];
|
||||
await nextTick();
|
||||
|
||||
expect(h.inputs.credentialSelections.value).toEqual({ 'HTTP Request': {} });
|
||||
});
|
||||
|
||||
it('seeds only newly added sections without overwriting existing user selections', async () => {
|
||||
addCredential({ id: 'b-cred', type: 'slackApi', name: 'B credential' });
|
||||
const h = setupHarness();
|
||||
|
||||
h.inputs.setCredential(h.sectionA, 'user-cred-a');
|
||||
|
||||
const sectionC = makeWorkflowSetupSection({
|
||||
id: 'GitHub:githubApi',
|
||||
targetNodeName: 'GitHub',
|
||||
credentialType: 'githubApi',
|
||||
currentCredentialId: 'github-cred',
|
||||
});
|
||||
addCredential({ id: 'github-cred', type: 'githubApi', name: 'GitHub credential' });
|
||||
|
||||
h.sectionsRef.value = [...h.sectionsRef.value, sectionC];
|
||||
await nextTick();
|
||||
|
||||
expect(h.inputs.credentialSelections.value).toEqual({
|
||||
'HTTP Request': { httpBasicAuth: 'user-cred-a' },
|
||||
GitHub: { githubApi: 'github-cred' },
|
||||
});
|
||||
});
|
||||
|
||||
it('re-seeds a section that is removed and later added back', async () => {
|
||||
addCredential({ id: 'current-cred', type: 'httpBasicAuth', name: 'Current credential' });
|
||||
const section = makeWorkflowSetupSection({
|
||||
targetNodeName: 'HTTP Request',
|
||||
credentialType: 'httpBasicAuth',
|
||||
currentCredentialId: 'current-cred',
|
||||
});
|
||||
|
||||
const h = setupHarness([section]);
|
||||
await nextTick();
|
||||
|
||||
h.inputs.setCredential(section, null);
|
||||
expect(h.inputs.credentialSelections.value).toEqual({ 'HTTP Request': {} });
|
||||
|
||||
h.sectionsRef.value = [];
|
||||
await nextTick();
|
||||
|
||||
h.sectionsRef.value = [section];
|
||||
await nextTick();
|
||||
|
||||
expect(h.inputs.credentialSelections.value).toEqual({
|
||||
'HTTP Request': { httpBasicAuth: 'current-cred' },
|
||||
});
|
||||
});
|
||||
|
||||
it('prunes skipped section ids that no longer correspond to a section', async () => {
|
||||
const h = setupHarness();
|
||||
h.inputs.markSectionSkipped(h.sectionA);
|
||||
h.inputs.markSectionSkipped(h.sectionB);
|
||||
|
||||
h.sectionsRef.value = [h.sectionB];
|
||||
await nextTick();
|
||||
|
||||
expect(h.inputs.isSectionSkipped(h.sectionA)).toBe(false);
|
||||
expect(h.inputs.isSectionSkipped(h.sectionB)).toBe(true);
|
||||
});
|
||||
|
||||
it('clears a skip when the skipped section later becomes complete', async () => {
|
||||
const h = setupHarness();
|
||||
credentialTest.testableTypes.add('httpBasicAuth');
|
||||
h.inputs.setCredential(h.sectionA, 'cred-1');
|
||||
h.inputs.markSectionSkipped(h.sectionA);
|
||||
|
||||
credentialsStore.credentialTestResults.set('cred-1', 'success');
|
||||
h.sectionsRef.value = [...h.sectionsRef.value];
|
||||
await nextTick();
|
||||
|
||||
expect(h.inputs.isSectionSkipped(h.sectionA)).toBe(false);
|
||||
});
|
||||
|
||||
it('tracks parameter values and builds nodeParameters after issues clear', () => {
|
||||
nodeTypesStore.getNodeType.mockReturnValue({
|
||||
name: 'n8n-nodes-base.httpRequest',
|
||||
properties: [
|
||||
{ displayName: 'URL', name: 'url', type: 'string', default: '', required: true },
|
||||
],
|
||||
});
|
||||
const parameterSection = makeWorkflowSetupSection({
|
||||
id: 'HTTP Request:parameters',
|
||||
credentialType: undefined,
|
||||
parameterNames: ['url'],
|
||||
node: { parameters: { url: '' } },
|
||||
});
|
||||
const h = setupHarness([parameterSection]);
|
||||
|
||||
expect(h.inputs.isSectionComplete(parameterSection)).toBe(false);
|
||||
|
||||
h.inputs.setParameterValue(parameterSection, 'url', 'https://example.com/api');
|
||||
|
||||
expect(h.inputs.isSectionComplete(parameterSection)).toBe(true);
|
||||
expect(h.inputs.buildCompletedSetupPayload()).toEqual({
|
||||
nodeParameters: {
|
||||
'HTTP Request': { url: 'https://example.com/api' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('updates nested parameter values without flattening the path', () => {
|
||||
const parameterSection = makeWorkflowSetupSection({
|
||||
id: 'HTTP Request:parameters',
|
||||
credentialType: undefined,
|
||||
parameterNames: ['options'],
|
||||
node: { parameters: { options: { path: 'old', keep: true } } },
|
||||
});
|
||||
const h = setupHarness([parameterSection]);
|
||||
|
||||
h.inputs.setParameterValue(parameterSection, 'options.path', 'new');
|
||||
|
||||
expect(h.inputs.getDisplayNode(parameterSection).parameters).toEqual({
|
||||
options: { path: 'new', keep: true },
|
||||
});
|
||||
expect(parameterSection.node.parameters).toEqual({ options: { path: 'old', keep: true } });
|
||||
expect(h.inputs.buildCompletedSetupPayload()).toEqual({
|
||||
nodeParameters: {
|
||||
'HTTP Request': { options: { path: 'new', keep: true } },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('mirrors a primary credential selection across grouped target nodes', () => {
|
||||
const groupedSection = makeWorkflowSetupSection({
|
||||
id: 'Primary:httpBasicAuth',
|
||||
targetNodeName: 'Primary',
|
||||
credentialType: 'httpBasicAuth',
|
||||
credentialTargetNodes: [
|
||||
{ id: 'primary', name: 'Primary', type: 'n8n-nodes-base.httpRequest' },
|
||||
{ id: 'follower', name: 'Follower', type: 'n8n-nodes-base.httpRequest' },
|
||||
],
|
||||
});
|
||||
const h = setupHarness([groupedSection]);
|
||||
|
||||
h.inputs.setCredential(groupedSection, 'cred-1');
|
||||
|
||||
expect(h.inputs.credentialSelections.value).toEqual({
|
||||
Primary: { httpBasicAuth: 'cred-1' },
|
||||
Follower: { httpBasicAuth: 'cred-1' },
|
||||
});
|
||||
expect(h.inputs.buildCompletedSetupPayload()).toEqual({
|
||||
nodeCredentials: {
|
||||
Primary: { httpBasicAuth: 'cred-1' },
|
||||
Follower: { httpBasicAuth: 'cred-1' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('clears mirrored credential selections across grouped target nodes', () => {
|
||||
const groupedSection = makeWorkflowSetupSection({
|
||||
id: 'Primary:httpBasicAuth',
|
||||
targetNodeName: 'Primary',
|
||||
credentialType: 'httpBasicAuth',
|
||||
credentialTargetNodes: [
|
||||
{ id: 'primary', name: 'Primary', type: 'n8n-nodes-base.httpRequest' },
|
||||
{ id: 'follower', name: 'Follower', type: 'n8n-nodes-base.httpRequest' },
|
||||
],
|
||||
});
|
||||
const h = setupHarness([groupedSection]);
|
||||
h.inputs.setCredential(groupedSection, 'cred-1');
|
||||
|
||||
h.inputs.setCredential(groupedSection, null);
|
||||
|
||||
expect(h.inputs.credentialSelections.value).toEqual({
|
||||
Primary: {},
|
||||
Follower: {},
|
||||
});
|
||||
expect(h.inputs.buildCompletedSetupPayload()).toEqual({});
|
||||
});
|
||||
|
||||
it('seeds current credentials across grouped target nodes', async () => {
|
||||
addCredential({ id: 'current-cred', type: 'httpBasicAuth', name: 'Current credential' });
|
||||
const groupedSection = makeWorkflowSetupSection({
|
||||
id: 'Primary:httpBasicAuth',
|
||||
targetNodeName: 'Primary',
|
||||
credentialType: 'httpBasicAuth',
|
||||
currentCredentialId: 'current-cred',
|
||||
credentialTargetNodes: [
|
||||
{ id: 'primary', name: 'Primary', type: 'n8n-nodes-base.httpRequest' },
|
||||
{ id: 'follower', name: 'Follower', type: 'n8n-nodes-base.httpRequest' },
|
||||
],
|
||||
});
|
||||
|
||||
const h = setupHarness([groupedSection]);
|
||||
await nextTick();
|
||||
|
||||
expect(h.inputs.credentialSelections.value).toEqual({
|
||||
Primary: { httpBasicAuth: 'current-cred' },
|
||||
Follower: { httpBasicAuth: 'current-cred' },
|
||||
});
|
||||
});
|
||||
|
||||
it('writes only to an independent params-bearing section target', () => {
|
||||
const groupedSection = makeWorkflowSetupSection({
|
||||
id: 'Primary:httpBasicAuth',
|
||||
targetNodeName: 'Primary',
|
||||
credentialType: 'httpBasicAuth',
|
||||
credentialTargetNodes: [
|
||||
{ id: 'primary', name: 'Primary', type: 'n8n-nodes-base.httpRequest' },
|
||||
{ id: 'follower', name: 'Follower', type: 'n8n-nodes-base.httpRequest' },
|
||||
],
|
||||
});
|
||||
const paramsSection = makeWorkflowSetupSection({
|
||||
id: 'Params:httpBasicAuth',
|
||||
targetNodeName: 'Params',
|
||||
credentialType: 'httpBasicAuth',
|
||||
parameterNames: ['url'],
|
||||
credentialTargetNodes: [{ id: 'params', name: 'Params', type: 'n8n-nodes-base.httpRequest' }],
|
||||
});
|
||||
const h = setupHarness([groupedSection, paramsSection]);
|
||||
|
||||
h.inputs.setCredential(paramsSection, 'cred-params');
|
||||
|
||||
expect(h.inputs.credentialSelections.value).toEqual({
|
||||
Params: { httpBasicAuth: 'cred-params' },
|
||||
});
|
||||
});
|
||||
|
||||
it('excludes skipped grouped sections from completed setup payloads', () => {
|
||||
const groupedSection = makeWorkflowSetupSection({
|
||||
id: 'Primary:httpBasicAuth',
|
||||
targetNodeName: 'Primary',
|
||||
credentialType: 'httpBasicAuth',
|
||||
credentialTargetNodes: [
|
||||
{ id: 'primary', name: 'Primary', type: 'n8n-nodes-base.httpRequest' },
|
||||
{ id: 'follower', name: 'Follower', type: 'n8n-nodes-base.httpRequest' },
|
||||
],
|
||||
});
|
||||
const h = setupHarness([groupedSection]);
|
||||
h.inputs.setCredential(groupedSection, 'cred-1');
|
||||
h.inputs.markSectionSkipped(groupedSection);
|
||||
|
||||
expect(h.inputs.buildCompletedSetupPayload()).toEqual({});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,282 @@
|
|||
import { computed, ref, watch, type ComputedRef, type Ref } from 'vue';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import { deepCopy, type INodeParameters } from 'n8n-workflow';
|
||||
import { setParameterValue as setParameterValueByPath } from '@/app/utils/parameterUtils';
|
||||
import { useCredentialsStore } from '@/features/credentials/credentials.store';
|
||||
import { useCredentialTestInBackground } from '@/features/credentials/composables/useCredentialTestInBackground';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import type { WorkflowSetupApplyPayload, WorkflowSetupSection } from '../workflowSetup.types';
|
||||
import { getWorkflowSetupParameterIssues } from '../workflowSetupParameterIssues';
|
||||
|
||||
export type CredentialSelectionsMap = Record<string, Record<string, string>>;
|
||||
type ParameterValuesMap = Record<string, INodeParameters>;
|
||||
|
||||
export function useWorkflowSetupInputs(deps: {
|
||||
sections: ComputedRef<WorkflowSetupSection[]>;
|
||||
}): {
|
||||
credentialSelections: Ref<CredentialSelectionsMap>;
|
||||
skippedSectionIds: Ref<Set<string>>;
|
||||
setCredential: (section: WorkflowSetupSection, credId: string | null) => void;
|
||||
setParameterValue: (section: WorkflowSetupSection, parameterName: string, value: unknown) => void;
|
||||
getDisplayNode: (section: WorkflowSetupSection) => INodeUi;
|
||||
isSectionComplete: (section: WorkflowSetupSection) => boolean;
|
||||
isCredentialTestFailed: (section: WorkflowSetupSection) => boolean;
|
||||
isSectionSkipped: (section: WorkflowSetupSection) => boolean;
|
||||
markSectionSkipped: (section: WorkflowSetupSection) => void;
|
||||
buildCompletedSetupPayload: () => WorkflowSetupApplyPayload;
|
||||
} {
|
||||
const credentialsStore = useCredentialsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const { isCredentialTypeTestable, testCredentialInBackground } = useCredentialTestInBackground();
|
||||
|
||||
const credentialSelections = ref<CredentialSelectionsMap>({});
|
||||
const parameterValues = ref<ParameterValuesMap>({});
|
||||
const skippedSectionIds = ref<Set<string>>(new Set());
|
||||
|
||||
function testCredential(credId: string, credType: string) {
|
||||
const credential = credentialsStore.getCredentialById(credId);
|
||||
if (!credential) return;
|
||||
|
||||
void testCredentialInBackground(credId, credential.name, credType);
|
||||
}
|
||||
|
||||
function setCredential(section: WorkflowSetupSection, credId: string | null) {
|
||||
if (!section.credentialType) return;
|
||||
|
||||
const targetNames = section.credentialTargetNodes.map((target) => target.name);
|
||||
const nextCredentialSelections = setCredentialSelectionForTargetNames(
|
||||
credentialSelections.value,
|
||||
targetNames,
|
||||
section.credentialType,
|
||||
credId,
|
||||
);
|
||||
|
||||
if (credId) {
|
||||
testCredential(credId, section.credentialType);
|
||||
clearSectionSkipped(section);
|
||||
}
|
||||
|
||||
credentialSelections.value = nextCredentialSelections;
|
||||
}
|
||||
|
||||
function setParameterValue(section: WorkflowSetupSection, parameterName: string, value: unknown) {
|
||||
const next = deepCopy(getParameterValues(section));
|
||||
setParameterValueByPath(next, parameterName, value);
|
||||
parameterValues.value = {
|
||||
...parameterValues.value,
|
||||
[section.targetNodeName]: next,
|
||||
};
|
||||
clearSectionSkipped(section);
|
||||
}
|
||||
|
||||
function isSectionSkipped(section: WorkflowSetupSection): boolean {
|
||||
return skippedSectionIds.value.has(section.id);
|
||||
}
|
||||
|
||||
function markSectionSkipped(section: WorkflowSetupSection): void {
|
||||
skippedSectionIds.value.add(section.id);
|
||||
}
|
||||
|
||||
function clearSectionSkipped(section: WorkflowSetupSection): void {
|
||||
skippedSectionIds.value.delete(section.id);
|
||||
}
|
||||
|
||||
function getDisplayNode(section: WorkflowSetupSection): INodeUi {
|
||||
return {
|
||||
...section.node,
|
||||
parameters: getParameterValues(section),
|
||||
} as INodeUi;
|
||||
}
|
||||
|
||||
function getParameterValues(section: WorkflowSetupSection): INodeParameters {
|
||||
return (
|
||||
parameterValues.value[section.targetNodeName] ?? (section.node.parameters as INodeParameters)
|
||||
);
|
||||
}
|
||||
|
||||
function getSelectedCredentialId(section: WorkflowSetupSection): string | undefined {
|
||||
if (!section.credentialType) return undefined;
|
||||
return credentialSelections.value[section.targetNodeName]?.[section.credentialType];
|
||||
}
|
||||
|
||||
const parameterIssuesBySectionId = computed(() => {
|
||||
const result = new Map<string, Record<string, string[]>>();
|
||||
for (const section of deps.sections.value) {
|
||||
if (section.parameterNames.length === 0) {
|
||||
result.set(section.id, {});
|
||||
continue;
|
||||
}
|
||||
const node = getDisplayNode(section);
|
||||
const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
||||
result.set(
|
||||
section.id,
|
||||
getWorkflowSetupParameterIssues(node, nodeType, section.parameterNames),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
function getParameterIssues(section: WorkflowSetupSection): Record<string, string[]> {
|
||||
return parameterIssuesBySectionId.value.get(section.id) ?? {};
|
||||
}
|
||||
|
||||
function isCredentialComplete(section: WorkflowSetupSection): boolean {
|
||||
if (!section.credentialType) return true;
|
||||
const selectedCredentialId = getSelectedCredentialId(section);
|
||||
if (!selectedCredentialId) return false;
|
||||
if (!isCredentialTypeTestable(section.credentialType)) return true;
|
||||
return credentialsStore.isCredentialTestedOk(selectedCredentialId);
|
||||
}
|
||||
|
||||
function areParametersComplete(section: WorkflowSetupSection): boolean {
|
||||
return Object.keys(getParameterIssues(section)).length === 0;
|
||||
}
|
||||
|
||||
function isSectionComplete(section: WorkflowSetupSection): boolean {
|
||||
return isCredentialComplete(section) && areParametersComplete(section);
|
||||
}
|
||||
|
||||
function isCredentialTestFailed(section: WorkflowSetupSection): boolean {
|
||||
if (!section.credentialType) return false;
|
||||
const selectedCredentialId = getSelectedCredentialId(section);
|
||||
if (!selectedCredentialId || !isCredentialTypeTestable(section.credentialType)) return false;
|
||||
return credentialsStore.credentialTestResults.get(selectedCredentialId) === 'error';
|
||||
}
|
||||
|
||||
function buildCompletedSetupPayload(): WorkflowSetupApplyPayload {
|
||||
const includeCredential = (section: WorkflowSetupSection) =>
|
||||
!isSectionSkipped(section) && isCredentialComplete(section);
|
||||
const includeParams = (section: WorkflowSetupSection) =>
|
||||
!isSectionSkipped(section) && areParametersComplete(section);
|
||||
|
||||
const nodeCredentials = buildNodeCredentials(includeCredential);
|
||||
const nodeParameters = buildNodeParameters(includeParams);
|
||||
|
||||
return {
|
||||
...(Object.keys(nodeCredentials).length > 0 ? { nodeCredentials } : {}),
|
||||
...(Object.keys(nodeParameters).length > 0 ? { nodeParameters } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function buildNodeCredentials(
|
||||
shouldInclude: (section: WorkflowSetupSection) => boolean,
|
||||
): Record<string, Record<string, string>> {
|
||||
const out: Record<string, Record<string, string>> = {};
|
||||
for (const section of deps.sections.value) {
|
||||
if (!section.credentialType || !shouldInclude(section)) continue;
|
||||
for (const target of section.credentialTargetNodes) {
|
||||
const credId = credentialSelections.value[target.name]?.[section.credentialType];
|
||||
if (!credId) continue;
|
||||
const perType = out[target.name] ?? {};
|
||||
perType[section.credentialType] = credId;
|
||||
out[target.name] = perType;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function buildNodeParameters(
|
||||
shouldInclude: (section: WorkflowSetupSection) => boolean,
|
||||
): Record<string, INodeParameters> {
|
||||
const out: Record<string, INodeParameters> = {};
|
||||
for (const section of deps.sections.value) {
|
||||
if (section.parameterNames.length === 0 || !shouldInclude(section)) continue;
|
||||
const values = getParameterValues(section);
|
||||
const params: INodeParameters = {};
|
||||
for (const name of section.parameterNames) {
|
||||
if (values[name] !== undefined) params[name] = values[name];
|
||||
}
|
||||
if (Object.keys(params).length > 0) out[section.targetNodeName] = params;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function seedCredentialSelectionsForNewSections(
|
||||
newSections: WorkflowSetupSection[],
|
||||
): Array<{ id: string; type: string }> {
|
||||
let nextCredentialSelections: CredentialSelectionsMap | null = null;
|
||||
const credentialsToTest: Array<{ id: string; type: string }> = [];
|
||||
|
||||
for (const section of newSections) {
|
||||
if (!section.credentialType) continue;
|
||||
if (!section.currentCredentialId) continue;
|
||||
|
||||
nextCredentialSelections = setCredentialSelectionForTargetNames(
|
||||
nextCredentialSelections ?? credentialSelections.value,
|
||||
section.credentialTargetNodes.map((target) => target.name),
|
||||
section.credentialType,
|
||||
section.currentCredentialId,
|
||||
);
|
||||
credentialsToTest.push({ id: section.currentCredentialId, type: section.credentialType });
|
||||
}
|
||||
|
||||
if (nextCredentialSelections) credentialSelections.value = nextCredentialSelections;
|
||||
return credentialsToTest;
|
||||
}
|
||||
|
||||
function pruneSkippedSectionsMissingFrom(sections: WorkflowSetupSection[]) {
|
||||
if (skippedSectionIds.value.size === 0) return;
|
||||
const knownIds = new Set(sections.map((s) => s.id));
|
||||
for (const id of skippedSectionIds.value) {
|
||||
if (!knownIds.has(id)) skippedSectionIds.value.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
deps.sections,
|
||||
(sections, oldSections) => {
|
||||
const previousSectionIds = new Set(oldSections?.map((section) => section.id) ?? []);
|
||||
const newSections = sections.filter((section) => !previousSectionIds.has(section.id));
|
||||
|
||||
const credentialsToTest = seedCredentialSelectionsForNewSections(newSections);
|
||||
for (const credential of credentialsToTest) {
|
||||
testCredential(credential.id, credential.type);
|
||||
}
|
||||
pruneSkippedSectionsMissingFrom(sections);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() =>
|
||||
deps.sections.value
|
||||
.filter((section) => skippedSectionIds.value.has(section.id) && isSectionComplete(section))
|
||||
.map((s) => s.id),
|
||||
(completedSkippedIds) => {
|
||||
for (const id of completedSkippedIds) {
|
||||
skippedSectionIds.value.delete(id);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
credentialSelections,
|
||||
skippedSectionIds,
|
||||
setCredential,
|
||||
setParameterValue,
|
||||
getDisplayNode,
|
||||
isSectionComplete,
|
||||
isCredentialTestFailed,
|
||||
isSectionSkipped,
|
||||
markSectionSkipped,
|
||||
buildCompletedSetupPayload,
|
||||
};
|
||||
}
|
||||
|
||||
function setCredentialSelectionForTargetNames(
|
||||
currentCredentialSelections: CredentialSelectionsMap,
|
||||
targetNames: string[],
|
||||
credentialType: string,
|
||||
credentialId: string | null,
|
||||
): CredentialSelectionsMap {
|
||||
const nextCredentialSelections = { ...currentCredentialSelections };
|
||||
|
||||
for (const targetName of targetNames) {
|
||||
const nodeCredentialSelections = { ...(nextCredentialSelections[targetName] ?? {}) };
|
||||
if (credentialId) nodeCredentialSelections[credentialType] = credentialId;
|
||||
else delete nodeCredentialSelections[credentialType];
|
||||
nextCredentialSelections[targetName] = nodeCredentialSelections;
|
||||
}
|
||||
|
||||
return nextCredentialSelections;
|
||||
}
|
||||
|
|
@ -0,0 +1,422 @@
|
|||
import { ref } from 'vue';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { makeSetupRequest } from '../__tests__/factories';
|
||||
import { useWorkflowSetupSections } from './useWorkflowSetupSections';
|
||||
|
||||
const credentialsStore = vi.hoisted(() => ({
|
||||
allCredentials: [] as Array<{ id: string; type: string; name: string }>,
|
||||
}));
|
||||
|
||||
const nodeTypesStore = vi.hoisted(() => ({
|
||||
getNodeType: vi.fn((): unknown => null),
|
||||
}));
|
||||
|
||||
vi.mock('@/features/credentials/credentials.store', () => ({
|
||||
useCredentialsStore: () => credentialsStore,
|
||||
}));
|
||||
|
||||
vi.mock('@/app/stores/nodeTypes.store', () => ({
|
||||
useNodeTypesStore: () => nodeTypesStore,
|
||||
}));
|
||||
|
||||
describe('useWorkflowSetupSections', () => {
|
||||
beforeEach(() => {
|
||||
credentialsStore.allCredentials = [];
|
||||
nodeTypesStore.getNodeType.mockReset();
|
||||
nodeTypesStore.getNodeType.mockReturnValue(null);
|
||||
});
|
||||
|
||||
it('skips setup requests without a credential type', () => {
|
||||
const setupRequests = ref([
|
||||
makeSetupRequest({ credentialType: undefined }),
|
||||
makeSetupRequest({ credentialType: 'httpBasicAuth' }),
|
||||
]);
|
||||
|
||||
const { sections } = useWorkflowSetupSections(setupRequests);
|
||||
|
||||
expect(sections.value).toHaveLength(1);
|
||||
expect(sections.value[0].credentialType).toBe('httpBasicAuth');
|
||||
});
|
||||
|
||||
it('creates sections for editable parameter-only setup requests', () => {
|
||||
const setupRequests = ref([
|
||||
makeSetupRequest({
|
||||
credentialType: undefined,
|
||||
parameterIssues: { url: ['URL is required'] },
|
||||
editableParameters: [{ name: 'url', displayName: 'URL', type: 'string' }],
|
||||
}),
|
||||
]);
|
||||
|
||||
const { sections } = useWorkflowSetupSections(setupRequests);
|
||||
|
||||
expect(sections.value).toHaveLength(1);
|
||||
expect(sections.value[0]).toMatchObject({
|
||||
id: 'HTTP Request:parameters',
|
||||
parameterNames: ['url'],
|
||||
});
|
||||
expect(sections.value[0].credentialType).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not create parameter-only sections for non-editable issues', () => {
|
||||
const setupRequests = ref([
|
||||
makeSetupRequest({
|
||||
credentialType: undefined,
|
||||
parameterIssues: { url: ['URL is required'] },
|
||||
}),
|
||||
]);
|
||||
|
||||
const { sections } = useWorkflowSetupSections(setupRequests);
|
||||
|
||||
expect(sections.value).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('resolves hidden parameter defaults from the node type', () => {
|
||||
nodeTypesStore.getNodeType.mockReturnValue({
|
||||
name: 'n8n-nodes-base.httpRequest',
|
||||
properties: [
|
||||
{ displayName: 'Method', name: 'method', type: 'options', default: 'GET' },
|
||||
{ displayName: 'URL', name: 'url', type: 'string', default: '' },
|
||||
],
|
||||
});
|
||||
const setupRequests = ref([
|
||||
makeSetupRequest({
|
||||
credentialType: undefined,
|
||||
parameterIssues: { url: ['URL is required'] },
|
||||
editableParameters: [{ name: 'url', displayName: 'URL', type: 'string' }],
|
||||
node: { parameters: { url: '' } },
|
||||
}),
|
||||
]);
|
||||
|
||||
const { sections } = useWorkflowSetupSections(setupRequests);
|
||||
|
||||
expect(sections.value[0].node.parameters).toMatchObject({ method: 'GET', url: '' });
|
||||
});
|
||||
|
||||
it('uses a stable node-name and credential-type id', () => {
|
||||
const setupRequests = ref([
|
||||
makeSetupRequest({
|
||||
credentialType: 'slackApi',
|
||||
node: { name: 'Slack' },
|
||||
}),
|
||||
]);
|
||||
|
||||
const { sections } = useWorkflowSetupSections(setupRequests);
|
||||
|
||||
expect(sections.value[0]).toMatchObject({
|
||||
id: 'Slack:slackApi',
|
||||
credentialType: 'slackApi',
|
||||
targetNodeName: 'Slack',
|
||||
});
|
||||
});
|
||||
|
||||
it('prefers the credential already assigned on the node', () => {
|
||||
credentialsStore.allCredentials = [
|
||||
{ id: 'store-cred', type: 'httpBasicAuth', name: 'Store credential' },
|
||||
];
|
||||
const setupRequests = ref([
|
||||
makeSetupRequest({
|
||||
credentialType: 'httpBasicAuth',
|
||||
node: {
|
||||
credentials: {
|
||||
httpBasicAuth: { id: 'node-cred', name: 'Node credential' },
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const { sections } = useWorkflowSetupSections(setupRequests);
|
||||
|
||||
expect(sections.value[0].currentCredentialId).toBe('node-cred');
|
||||
});
|
||||
|
||||
it('does not preselect unrelated store credentials', () => {
|
||||
credentialsStore.allCredentials = [
|
||||
{ id: 'other-cred', type: 'slackApi', name: 'Slack credential' },
|
||||
{ id: 'matching-cred', type: 'httpBasicAuth', name: 'HTTP credential' },
|
||||
];
|
||||
const setupRequests = ref([makeSetupRequest({ credentialType: 'httpBasicAuth' })]);
|
||||
|
||||
const { sections } = useWorkflowSetupSections(setupRequests);
|
||||
|
||||
expect(sections.value[0].currentCredentialId).toBeNull();
|
||||
});
|
||||
|
||||
it('uses null when no assigned or matching store credential exists', () => {
|
||||
credentialsStore.allCredentials = [
|
||||
{ id: 'other-cred', type: 'slackApi', name: 'Slack credential' },
|
||||
];
|
||||
const setupRequests = ref([makeSetupRequest({ credentialType: 'httpBasicAuth' })]);
|
||||
|
||||
const { sections } = useWorkflowSetupSections(setupRequests);
|
||||
|
||||
expect(sections.value[0].currentCredentialId).toBeNull();
|
||||
});
|
||||
|
||||
it('updates when setup requests change', () => {
|
||||
const setupRequests = ref([makeSetupRequest({ credentialType: 'httpBasicAuth' })]);
|
||||
const { sections } = useWorkflowSetupSections(setupRequests);
|
||||
|
||||
setupRequests.value = [
|
||||
makeSetupRequest({ credentialType: 'slackApi', node: { name: 'Slack' } }),
|
||||
];
|
||||
|
||||
expect(sections.value).toHaveLength(1);
|
||||
expect(sections.value[0]).toMatchObject({
|
||||
id: 'Slack:slackApi',
|
||||
credentialType: 'slackApi',
|
||||
});
|
||||
});
|
||||
|
||||
it('groups cred-only requests with the same credential type into one primary section', () => {
|
||||
const setupRequests = ref([
|
||||
makeSetupRequest({ node: { id: 'first', name: 'First' } }),
|
||||
makeSetupRequest({ node: { id: 'second', name: 'Second' } }),
|
||||
]);
|
||||
|
||||
const { sections } = useWorkflowSetupSections(setupRequests);
|
||||
|
||||
expect(sections.value).toHaveLength(1);
|
||||
expect(sections.value[0]).toMatchObject({ id: 'First:httpBasicAuth', targetNodeName: 'First' });
|
||||
expect(sections.value[0].credentialTargetNodes).toEqual([
|
||||
{ id: 'first', name: 'First', type: 'n8n-nodes-base.httpRequest' },
|
||||
{ id: 'second', name: 'Second', type: 'n8n-nodes-base.httpRequest' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('creates one primary per credential type', () => {
|
||||
const setupRequests = ref([
|
||||
makeSetupRequest({ credentialType: 'httpBasicAuth', node: { id: 'first', name: 'First' } }),
|
||||
makeSetupRequest({ credentialType: 'slackApi', node: { id: 'second', name: 'Second' } }),
|
||||
makeSetupRequest({ credentialType: 'httpBasicAuth', node: { id: 'third', name: 'Third' } }),
|
||||
]);
|
||||
|
||||
const { sections } = useWorkflowSetupSections(setupRequests);
|
||||
|
||||
expect(sections.value).toHaveLength(2);
|
||||
expect(sections.value.map((section) => section.credentialType)).toEqual([
|
||||
'httpBasicAuth',
|
||||
'slackApi',
|
||||
]);
|
||||
expect(sections.value[0].credentialTargetNodes.map((target) => target.name)).toEqual([
|
||||
'First',
|
||||
'Third',
|
||||
]);
|
||||
expect(sections.value[1].credentialTargetNodes.map((target) => target.name)).toEqual([
|
||||
'Second',
|
||||
]);
|
||||
});
|
||||
|
||||
it('groups a cred-only follower into a primary section that has parameters', () => {
|
||||
const setupRequests = ref([
|
||||
makeSetupRequest({
|
||||
node: { id: 'primary', name: 'Primary' },
|
||||
editableParameters: [{ name: 'url', displayName: 'URL', type: 'string' }],
|
||||
}),
|
||||
makeSetupRequest({ node: { id: 'follower', name: 'Follower' } }),
|
||||
]);
|
||||
|
||||
const { sections } = useWorkflowSetupSections(setupRequests);
|
||||
|
||||
expect(sections.value).toHaveLength(1);
|
||||
expect(sections.value[0].parameterNames).toEqual(['url']);
|
||||
expect(sections.value[0].credentialTargetNodes.map((target) => target.name)).toEqual([
|
||||
'Primary',
|
||||
'Follower',
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps a params-bearing follower independent from a cred-only primary', () => {
|
||||
const setupRequests = ref([
|
||||
makeSetupRequest({ node: { id: 'primary', name: 'Primary' } }),
|
||||
makeSetupRequest({
|
||||
node: { id: 'follower', name: 'Follower' },
|
||||
editableParameters: [{ name: 'url', displayName: 'URL', type: 'string' }],
|
||||
}),
|
||||
]);
|
||||
|
||||
const { sections } = useWorkflowSetupSections(setupRequests);
|
||||
|
||||
expect(sections.value).toHaveLength(2);
|
||||
expect(sections.value[0].credentialTargetNodes.map((target) => target.name)).toEqual([
|
||||
'Primary',
|
||||
]);
|
||||
expect(sections.value[1].parameterNames).toEqual(['url']);
|
||||
expect(sections.value[1].credentialTargetNodes.map((target) => target.name)).toEqual([
|
||||
'Follower',
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps params-bearing requests with the same credential type independent', () => {
|
||||
const setupRequests = ref([
|
||||
makeSetupRequest({
|
||||
node: { id: 'primary', name: 'Primary' },
|
||||
editableParameters: [{ name: 'url', displayName: 'URL', type: 'string' }],
|
||||
}),
|
||||
makeSetupRequest({
|
||||
node: { id: 'follower', name: 'Follower' },
|
||||
editableParameters: [{ name: 'path', displayName: 'Path', type: 'string' }],
|
||||
}),
|
||||
]);
|
||||
|
||||
const { sections } = useWorkflowSetupSections(setupRequests);
|
||||
|
||||
expect(sections.value).toHaveLength(2);
|
||||
expect(sections.value[0].credentialTargetNodes.map((target) => target.name)).toEqual([
|
||||
'Primary',
|
||||
]);
|
||||
expect(sections.value[1].credentialTargetNodes.map((target) => target.name)).toEqual([
|
||||
'Follower',
|
||||
]);
|
||||
});
|
||||
|
||||
it('groups HTTP requests by the same literal URL and splits different URLs', () => {
|
||||
const setupRequests = ref([
|
||||
makeSetupRequest({
|
||||
node: { id: 'first', name: 'First', parameters: { url: 'https://a.test' } },
|
||||
}),
|
||||
makeSetupRequest({
|
||||
node: { id: 'second', name: 'Second', parameters: { url: 'https://a.test' } },
|
||||
}),
|
||||
makeSetupRequest({
|
||||
node: { id: 'third', name: 'Third', parameters: { url: 'https://b.test' } },
|
||||
}),
|
||||
]);
|
||||
|
||||
const { sections } = useWorkflowSetupSections(setupRequests);
|
||||
|
||||
expect(sections.value).toHaveLength(2);
|
||||
expect(sections.value[0].credentialTargetNodes.map((target) => target.name)).toEqual([
|
||||
'First',
|
||||
'Second',
|
||||
]);
|
||||
expect(sections.value[1].credentialTargetNodes.map((target) => target.name)).toEqual(['Third']);
|
||||
});
|
||||
|
||||
it('splits HTTP expression URLs per node even when the expression string matches', () => {
|
||||
const setupRequests = ref([
|
||||
makeSetupRequest({
|
||||
node: { id: 'first', name: 'First', parameters: { url: '={{ $json.url }}' } },
|
||||
}),
|
||||
makeSetupRequest({
|
||||
node: { id: 'second', name: 'Second', parameters: { url: '={{ $json.url }}' } },
|
||||
}),
|
||||
]);
|
||||
|
||||
const { sections } = useWorkflowSetupSections(setupRequests);
|
||||
|
||||
expect(sections.value).toHaveLength(2);
|
||||
expect(sections.value[0].credentialTargetNodes.map((target) => target.name)).toEqual(['First']);
|
||||
expect(sections.value[1].credentialTargetNodes.map((target) => target.name)).toEqual([
|
||||
'Second',
|
||||
]);
|
||||
});
|
||||
|
||||
it('groups HTTP requests with missing or non-string URLs', () => {
|
||||
const setupRequests = ref([
|
||||
makeSetupRequest({ node: { id: 'first', name: 'First', parameters: {} } }),
|
||||
makeSetupRequest({ node: { id: 'second', name: 'Second', parameters: { url: 123 } } }),
|
||||
]);
|
||||
|
||||
const { sections } = useWorkflowSetupSections(setupRequests);
|
||||
|
||||
expect(sections.value).toHaveLength(1);
|
||||
expect(sections.value[0].credentialTargetNodes.map((target) => target.name)).toEqual([
|
||||
'First',
|
||||
'Second',
|
||||
]);
|
||||
});
|
||||
|
||||
it('groups non-HTTP requests only by credential type', () => {
|
||||
const setupRequests = ref([
|
||||
makeSetupRequest({
|
||||
node: { id: 'first', name: 'First', type: 'n8n-nodes-base.slack' },
|
||||
credentialType: 'slackApi',
|
||||
}),
|
||||
makeSetupRequest({
|
||||
node: { id: 'second', name: 'Second', type: 'n8n-nodes-base.slack' },
|
||||
credentialType: 'slackApi',
|
||||
}),
|
||||
]);
|
||||
|
||||
const { sections } = useWorkflowSetupSections(setupRequests);
|
||||
|
||||
expect(sections.value).toHaveLength(1);
|
||||
expect(sections.value[0].credentialTargetNodes.map((target) => target.name)).toEqual([
|
||||
'First',
|
||||
'Second',
|
||||
]);
|
||||
});
|
||||
|
||||
describe('subnode-root-aware credential merging', () => {
|
||||
const agentA = { name: 'Agent A', type: 'agent', typeVersion: 1, id: 'a' };
|
||||
const agentB = { name: 'Agent B', type: 'agent', typeVersion: 1, id: 'b' };
|
||||
|
||||
it('keeps two agents with HTTP-credential sub-nodes in separate sections', () => {
|
||||
const setupRequests = ref([
|
||||
makeSetupRequest({
|
||||
subnodeRootNode: agentA,
|
||||
node: { id: 'tool-a', name: 'Tool A', type: 'n8n-nodes-base.httpRequestTool' },
|
||||
}),
|
||||
makeSetupRequest({
|
||||
subnodeRootNode: agentB,
|
||||
node: { id: 'tool-b', name: 'Tool B', type: 'n8n-nodes-base.httpRequestTool' },
|
||||
}),
|
||||
]);
|
||||
|
||||
const { sections } = useWorkflowSetupSections(setupRequests);
|
||||
|
||||
expect(sections.value).toHaveLength(2);
|
||||
expect(sections.value[0].credentialTargetNodes.map((target) => target.name)).toEqual([
|
||||
'Tool A',
|
||||
]);
|
||||
expect(sections.value[1].credentialTargetNodes.map((target) => target.name)).toEqual([
|
||||
'Tool B',
|
||||
]);
|
||||
});
|
||||
|
||||
it('merges sub-nodes of the same agent that share a credential type', () => {
|
||||
const setupRequests = ref([
|
||||
makeSetupRequest({
|
||||
subnodeRootNode: agentA,
|
||||
node: { id: 'tool-1', name: 'Tool 1', type: 'n8n-nodes-base.slack' },
|
||||
credentialType: 'slackApi',
|
||||
}),
|
||||
makeSetupRequest({
|
||||
subnodeRootNode: agentA,
|
||||
node: { id: 'tool-2', name: 'Tool 2', type: 'n8n-nodes-base.slack' },
|
||||
credentialType: 'slackApi',
|
||||
}),
|
||||
]);
|
||||
|
||||
const { sections } = useWorkflowSetupSections(setupRequests);
|
||||
|
||||
expect(sections.value).toHaveLength(1);
|
||||
expect(sections.value[0].credentialTargetNodes.map((target) => target.name)).toEqual([
|
||||
'Tool 1',
|
||||
'Tool 2',
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps a standalone HTTP node and an agent sub-node in separate sections', () => {
|
||||
const setupRequests = ref([
|
||||
makeSetupRequest({
|
||||
node: { id: 'standalone', name: 'Standalone' },
|
||||
}),
|
||||
makeSetupRequest({
|
||||
subnodeRootNode: agentA,
|
||||
node: { id: 'tool-a', name: 'Tool A', type: 'n8n-nodes-base.httpRequestTool' },
|
||||
}),
|
||||
]);
|
||||
|
||||
const { sections } = useWorkflowSetupSections(setupRequests);
|
||||
|
||||
expect(sections.value).toHaveLength(2);
|
||||
expect(sections.value[0].credentialTargetNodes.map((target) => target.name)).toEqual([
|
||||
'Standalone',
|
||||
]);
|
||||
expect(sections.value[1].credentialTargetNodes.map((target) => target.name)).toEqual([
|
||||
'Tool A',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
import type { InstanceAiWorkflowSetupNode } from '@n8n/api-types';
|
||||
import { computed, type ComputedRef, type Ref } from 'vue';
|
||||
import { isExpression } from '@/app/utils/expressions';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { isHttpRequestNodeType } from '@/features/setupPanel/setupPanel.utils';
|
||||
import { NodeHelpers, type INodeParameters } from 'n8n-workflow';
|
||||
import type { WorkflowSetupSection } from '../workflowSetup.types';
|
||||
import { buildSectionId } from '../workflowSetup.helpers';
|
||||
|
||||
export function useWorkflowSetupSections(
|
||||
setupRequests: Ref<InstanceAiWorkflowSetupNode[]> | ComputedRef<InstanceAiWorkflowSetupNode[]>,
|
||||
): { sections: ComputedRef<WorkflowSetupSection[]> } {
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
|
||||
const sections = computed<WorkflowSetupSection[]>(() => {
|
||||
const result: WorkflowSetupSection[] = [];
|
||||
const primaryByGroupKey = new Map<string, WorkflowSetupSection>();
|
||||
|
||||
for (const req of setupRequests.value) {
|
||||
const parameterNames = (req.editableParameters ?? []).map((parameter) => parameter.name);
|
||||
if (!req.credentialType && parameterNames.length === 0) continue;
|
||||
|
||||
const credentialType = req.credentialType;
|
||||
const hasParams = parameterNames.length > 0;
|
||||
const groupKey = buildGroupKey(req, credentialType);
|
||||
const existingPrimary = groupKey ? primaryByGroupKey.get(groupKey) : undefined;
|
||||
|
||||
if (existingPrimary && !hasParams) {
|
||||
existingPrimary.credentialTargetNodes.push({
|
||||
id: req.node.id,
|
||||
name: req.node.name,
|
||||
type: req.node.type,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const node = {
|
||||
...req.node,
|
||||
parameters: resolveParameterDefaults(req.node),
|
||||
};
|
||||
const currentCredentialId =
|
||||
credentialType === undefined ? null : (req.node.credentials?.[credentialType]?.id ?? null);
|
||||
|
||||
const section: WorkflowSetupSection = {
|
||||
id: buildSectionId(req.node.name, credentialType),
|
||||
...(credentialType ? { credentialType } : {}),
|
||||
targetNodeName: req.node.name,
|
||||
node,
|
||||
currentCredentialId,
|
||||
parameterNames,
|
||||
credentialTargetNodes: [{ id: req.node.id, name: req.node.name, type: req.node.type }],
|
||||
};
|
||||
|
||||
result.push(section);
|
||||
if (groupKey && !existingPrimary) primaryByGroupKey.set(groupKey, section);
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
function resolveParameterDefaults(node: InstanceAiWorkflowSetupNode['node']): INodeParameters {
|
||||
const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
||||
if (!nodeType) return node.parameters as INodeParameters;
|
||||
|
||||
return (NodeHelpers.getNodeParameters(
|
||||
nodeType.properties,
|
||||
node.parameters as INodeParameters,
|
||||
true,
|
||||
true,
|
||||
node,
|
||||
nodeType,
|
||||
) ?? node.parameters) as INodeParameters;
|
||||
}
|
||||
|
||||
return { sections };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a merge key for credential-only sections.
|
||||
*
|
||||
* When the request is a sub-node (has `subnodeRootNode`), the root node's name is
|
||||
* prepended so credential sections never merge across different root nodes —
|
||||
* sub-nodes of two different agents stay separate even when they share a
|
||||
* credential type. Standalone nodes (no subnode root) keep the original
|
||||
* credentialType+URL merging behaviour to preserve the existing UX
|
||||
* optimisation of configuring a shared credential once.
|
||||
*/
|
||||
function buildGroupKey(
|
||||
req: InstanceAiWorkflowSetupNode,
|
||||
credentialType: string | undefined,
|
||||
): string | null {
|
||||
if (!credentialType) return null;
|
||||
|
||||
const rootPrefix = req.subnodeRootNode?.name ? `${req.subnodeRootNode.name}|` : '';
|
||||
const baseKey = `${rootPrefix}${credentialType}`;
|
||||
|
||||
if (!isHttpRequestNodeType(req.node.type)) return baseKey;
|
||||
|
||||
const url = req.node.parameters?.url;
|
||||
if (typeof url !== 'string') return `${baseKey}|http|none`;
|
||||
if (isExpression(url)) return `${baseKey}|http|expr|${req.node.name}`;
|
||||
return `${baseKey}|http|${url}`;
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
import { computed, ref } from 'vue';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { InstanceAiWorkflowSetupNode } from '@n8n/api-types';
|
||||
import { makeSetupRequest, makeWorkflowSetupSection } from '../__tests__/factories';
|
||||
import type { WorkflowSetupSection } from '../workflowSetup.types';
|
||||
import { useWorkflowSetupSteps } from './useWorkflowSetupSteps';
|
||||
|
||||
const agent = { name: 'Agent', type: 'agent', typeVersion: 1, id: 'agent-1' };
|
||||
const agentB = { name: 'Agent B', type: 'agent', typeVersion: 1, id: 'agent-b' };
|
||||
|
||||
function harness(sections: WorkflowSetupSection[], setupRequests: InstanceAiWorkflowSetupNode[]) {
|
||||
const sectionsRef = computed(() => sections);
|
||||
const setupRequestsRef = ref(setupRequests);
|
||||
return useWorkflowSetupSteps({ sections: sectionsRef, setupRequests: setupRequestsRef });
|
||||
}
|
||||
|
||||
describe('useWorkflowSetupSteps', () => {
|
||||
it('returns an empty list when there are no sections', () => {
|
||||
const { steps } = harness([], []);
|
||||
expect(steps.value).toEqual([]);
|
||||
});
|
||||
|
||||
it('mirrors sections 1:1 when no groupable connections exist', () => {
|
||||
const sections = [
|
||||
makeWorkflowSetupSection({ id: 'A:slackApi', targetNodeName: 'A' }),
|
||||
makeWorkflowSetupSection({ id: 'B:gmailApi', targetNodeName: 'B' }),
|
||||
];
|
||||
const requests = [
|
||||
makeSetupRequest({ node: { name: 'A' } }),
|
||||
makeSetupRequest({ node: { name: 'B' } }),
|
||||
];
|
||||
const { steps } = harness(sections, requests);
|
||||
|
||||
expect(steps.value).toHaveLength(2);
|
||||
expect(steps.value.every((s) => s.kind === 'section')).toBe(true);
|
||||
});
|
||||
|
||||
it('inserts the group at the first sub-node position when the root node comes after the sub-nodes', () => {
|
||||
const sections = [
|
||||
makeWorkflowSetupSection({ id: 'Model:openAiApi', targetNodeName: 'Model' }),
|
||||
makeWorkflowSetupSection({ id: 'Standalone:slackApi', targetNodeName: 'Standalone' }),
|
||||
makeWorkflowSetupSection({ id: 'Agent:foo', targetNodeName: 'Agent' }),
|
||||
];
|
||||
const requests = [
|
||||
makeSetupRequest({ node: { name: 'Model' }, subnodeRootNode: agent }),
|
||||
makeSetupRequest({ node: { name: 'Standalone' } }),
|
||||
makeSetupRequest({ node: { name: 'Agent' } }),
|
||||
];
|
||||
|
||||
const { steps } = harness(sections, requests);
|
||||
|
||||
expect(steps.value).toHaveLength(2);
|
||||
const groupStep = steps.value[0];
|
||||
if (groupStep.kind !== 'group') throw new Error('expected group step');
|
||||
expect(groupStep.group.subnodeRootNode.name).toBe('Agent');
|
||||
expect(groupStep.group.subnodeSections.map((s) => s.id)).toEqual(['Model:openAiApi']);
|
||||
expect(groupStep.group.rootSection?.id).toBe('Agent:foo');
|
||||
const sectionStep = steps.value[1];
|
||||
if (sectionStep.kind !== 'section') throw new Error('expected section step');
|
||||
expect(sectionStep.section.targetNodeName).toBe('Standalone');
|
||||
});
|
||||
|
||||
it('inserts the group at the root node position when the root comes before its sub-nodes', () => {
|
||||
const sections = [
|
||||
makeWorkflowSetupSection({ id: 'Agent:foo', targetNodeName: 'Agent' }),
|
||||
makeWorkflowSetupSection({ id: 'Standalone:slackApi', targetNodeName: 'Standalone' }),
|
||||
makeWorkflowSetupSection({ id: 'Model:openAiApi', targetNodeName: 'Model' }),
|
||||
];
|
||||
const requests = [
|
||||
makeSetupRequest({ node: { name: 'Agent' } }),
|
||||
makeSetupRequest({ node: { name: 'Standalone' } }),
|
||||
makeSetupRequest({ node: { name: 'Model' }, subnodeRootNode: agent }),
|
||||
];
|
||||
|
||||
const { steps } = harness(sections, requests);
|
||||
|
||||
expect(steps.value).toHaveLength(2);
|
||||
expect(steps.value[0].kind).toBe('group');
|
||||
const sectionStep = steps.value[1];
|
||||
if (sectionStep.kind !== 'section') throw new Error('expected section step');
|
||||
expect(sectionStep.section.targetNodeName).toBe('Standalone');
|
||||
});
|
||||
|
||||
it('emits a group with no rootSection when the root node has no setup request', () => {
|
||||
const sections = [makeWorkflowSetupSection({ id: 'Model:openAiApi', targetNodeName: 'Model' })];
|
||||
const requests = [makeSetupRequest({ node: { name: 'Model' }, subnodeRootNode: agent })];
|
||||
|
||||
const { steps } = harness(sections, requests);
|
||||
|
||||
expect(steps.value).toHaveLength(1);
|
||||
const step = steps.value[0];
|
||||
if (step.kind !== 'group') throw new Error('expected group');
|
||||
expect(step.group.rootSection).toBeUndefined();
|
||||
expect(step.group.subnodeSections.map((s) => s.id)).toEqual(['Model:openAiApi']);
|
||||
expect(step.group.subnodeRootNode).toEqual(agent);
|
||||
});
|
||||
|
||||
it('attaches multiple sections from the same sub-node into subnodeSections', () => {
|
||||
const sections = [
|
||||
makeWorkflowSetupSection({ id: 'Model:openAiApi', targetNodeName: 'Model' }),
|
||||
makeWorkflowSetupSection({
|
||||
id: 'Model:parameters',
|
||||
targetNodeName: 'Model',
|
||||
credentialType: undefined,
|
||||
parameterNames: ['temperature'],
|
||||
}),
|
||||
];
|
||||
const requests = [
|
||||
makeSetupRequest({ node: { name: 'Model' }, subnodeRootNode: agent }),
|
||||
makeSetupRequest({
|
||||
node: { name: 'Model' },
|
||||
subnodeRootNode: agent,
|
||||
credentialType: undefined,
|
||||
editableParameters: [{ name: 'temperature', displayName: 'Temperature', type: 'number' }],
|
||||
}),
|
||||
];
|
||||
|
||||
const { steps } = harness(sections, requests);
|
||||
|
||||
expect(steps.value).toHaveLength(1);
|
||||
const step = steps.value[0];
|
||||
if (step.kind !== 'group') throw new Error('expected group');
|
||||
expect(step.group.subnodeSections.map((s) => s.id)).toEqual([
|
||||
'Model:openAiApi',
|
||||
'Model:parameters',
|
||||
]);
|
||||
expect(step.group.rootSection).toBeUndefined();
|
||||
});
|
||||
|
||||
it('preserves execution order between two non-overlapping groups', () => {
|
||||
const sections = [
|
||||
makeWorkflowSetupSection({ id: 'ModelA:openAiApi', targetNodeName: 'Model A' }),
|
||||
makeWorkflowSetupSection({ id: 'ModelB:openAiApi', targetNodeName: 'Model B' }),
|
||||
];
|
||||
const requests = [
|
||||
makeSetupRequest({ node: { name: 'Model A' }, subnodeRootNode: agent }),
|
||||
makeSetupRequest({ node: { name: 'Model B' }, subnodeRootNode: agentB }),
|
||||
];
|
||||
|
||||
const { steps } = harness(sections, requests);
|
||||
|
||||
expect(steps.value).toHaveLength(2);
|
||||
const [first, second] = steps.value;
|
||||
if (first.kind !== 'group' || second.kind !== 'group') throw new Error('expected groups');
|
||||
expect(first.group.subnodeRootNode.name).toBe('Agent');
|
||||
expect(second.group.subnodeRootNode.name).toBe('Agent B');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import { computed, type ComputedRef, type Ref } from 'vue';
|
||||
import type { InstanceAiWorkflowSetupNode } from '@n8n/api-types';
|
||||
import type {
|
||||
WorkflowSetupGroup,
|
||||
WorkflowSetupSection,
|
||||
WorkflowSetupStep,
|
||||
} from '../workflowSetup.types';
|
||||
|
||||
/**
|
||||
* Combines flat sections with subnode-root metadata from setup requests to
|
||||
* produce a list of wizard steps. Sections that share a root node (an agent)
|
||||
* are folded into a single `{ group }` step emitted at the position of the
|
||||
* group's earliest section. Ungrouped sections pass through.
|
||||
*/
|
||||
export function useWorkflowSetupSteps(deps: {
|
||||
sections: ComputedRef<WorkflowSetupSection[]>;
|
||||
setupRequests: Ref<InstanceAiWorkflowSetupNode[]> | ComputedRef<InstanceAiWorkflowSetupNode[]>;
|
||||
}): { steps: ComputedRef<WorkflowSetupStep[]> } {
|
||||
const steps = computed<WorkflowSetupStep[]>(() => {
|
||||
const sections = deps.sections.value;
|
||||
const requests = deps.setupRequests.value;
|
||||
if (sections.length === 0) return [];
|
||||
|
||||
const rootBySubnodeName = new Map<string, WorkflowSetupGroup['subnodeRootNode']>();
|
||||
const rootMetaByName = new Map<string, WorkflowSetupGroup['subnodeRootNode']>();
|
||||
for (const req of requests) {
|
||||
if (!req.subnodeRootNode) continue;
|
||||
rootBySubnodeName.set(req.node.name, req.subnodeRootNode);
|
||||
if (!rootMetaByName.has(req.subnodeRootNode.name)) {
|
||||
rootMetaByName.set(req.subnodeRootNode.name, req.subnodeRootNode);
|
||||
}
|
||||
}
|
||||
|
||||
const result: WorkflowSetupStep[] = [];
|
||||
const groupByRootName = new Map<string, WorkflowSetupGroup>();
|
||||
|
||||
for (const section of sections) {
|
||||
const subnodeRoot = rootBySubnodeName.get(section.targetNodeName);
|
||||
const selfAsRoot = rootMetaByName.get(section.targetNodeName);
|
||||
const root = subnodeRoot ?? selfAsRoot;
|
||||
|
||||
if (!root) {
|
||||
result.push({ kind: 'section', section });
|
||||
continue;
|
||||
}
|
||||
|
||||
let group = groupByRootName.get(root.name);
|
||||
if (!group) {
|
||||
group = { subnodeRootNode: root, subnodeSections: [] };
|
||||
groupByRootName.set(root.name, group);
|
||||
result.push({ kind: 'group', group });
|
||||
}
|
||||
|
||||
if (subnodeRoot) {
|
||||
group.subnodeSections.push(section);
|
||||
} else if (!group.rootSection) {
|
||||
group.rootSection = section;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
return { steps };
|
||||
}
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
import { watch, type ComputedRef, type Ref } from 'vue';
|
||||
import { useTelemetry } from '@/app/composables/useTelemetry';
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import type { ThreadRuntime } from '../../instanceAi.store';
|
||||
import type { WorkflowSetupSection, WorkflowSetupStep } from '../workflowSetup.types';
|
||||
import { getStepSections } from '../workflowSetup.helpers';
|
||||
|
||||
type ProvidedSetupInput = { label: string; options: string[]; option_chosen: string };
|
||||
type SkippedSetupInput = { label: string; options: string[] };
|
||||
type SetupStepOutcome = 'completed' | 'skipped' | 'mixed';
|
||||
|
||||
type WorkflowSetupStepTelemetryInput = {
|
||||
input_type: 'credential' | 'parameter';
|
||||
node_type: string;
|
||||
credential_type?: string;
|
||||
parameter_name?: string;
|
||||
};
|
||||
|
||||
type WorkflowSetupStepTelemetryPayload = {
|
||||
thread_id: string;
|
||||
input_thread_id: string;
|
||||
instance_id: string;
|
||||
type: 'setup';
|
||||
request_id: string;
|
||||
step_index: number;
|
||||
step_count: number;
|
||||
step_kind: WorkflowSetupStep['kind'];
|
||||
setup_inputs: WorkflowSetupStepTelemetryInput[];
|
||||
outcome?: SetupStepOutcome;
|
||||
};
|
||||
|
||||
type SetupTelemetryContext = Pick<
|
||||
WorkflowSetupStepTelemetryPayload,
|
||||
'thread_id' | 'input_thread_id' | 'instance_id' | 'type'
|
||||
>;
|
||||
|
||||
interface WorkflowSetupTelemetryInputAccessors {
|
||||
isSectionComplete: (section: WorkflowSetupSection) => boolean;
|
||||
isSectionSkipped: (section: WorkflowSetupSection) => boolean;
|
||||
}
|
||||
|
||||
export function useWorkflowSetupTelemetry(deps: {
|
||||
requestId: Ref<string>;
|
||||
sections: ComputedRef<WorkflowSetupSection[]>;
|
||||
steps: ComputedRef<WorkflowSetupStep[]>;
|
||||
activeStep: ComputedRef<WorkflowSetupStep | undefined>;
|
||||
currentStepIndex: Ref<number>;
|
||||
isReady: Ref<boolean>;
|
||||
inputs: WorkflowSetupTelemetryInputAccessors;
|
||||
thread: ThreadRuntime;
|
||||
}) {
|
||||
const telemetry = useTelemetry();
|
||||
const rootStore = useRootStore();
|
||||
|
||||
const shownStepKeys = new Set<string>();
|
||||
const handledStepKeys = new Set<string>();
|
||||
|
||||
function getSetupTelemetryContext(): SetupTelemetryContext {
|
||||
const tc = deps.thread.findToolCallByRequestId(deps.requestId.value);
|
||||
return {
|
||||
thread_id: deps.thread.currentThreadId,
|
||||
input_thread_id: tc?.confirmation?.inputThreadId ?? '',
|
||||
instance_id: rootStore.instanceId,
|
||||
type: 'setup',
|
||||
};
|
||||
}
|
||||
|
||||
function getStepKey(step: WorkflowSetupStep): string {
|
||||
const inputKeys = getStepTelemetryInputs(step).map(
|
||||
(input) =>
|
||||
`${input.input_type}:${input.node_type}:${input.credential_type ?? input.parameter_name}`,
|
||||
);
|
||||
return `${step.kind}:${deps.steps.value.indexOf(step)}:${inputKeys.join('|')}`;
|
||||
}
|
||||
|
||||
function getTrackingStepKey(step: WorkflowSetupStep): string {
|
||||
return `${deps.requestId.value}:${getStepKey(step)}`;
|
||||
}
|
||||
|
||||
function getStepOutcome(step: WorkflowSetupStep): SetupStepOutcome | undefined {
|
||||
const sections = getStepSections(step);
|
||||
if (sections.length === 0) return undefined;
|
||||
|
||||
let completedCount = 0;
|
||||
let skippedCount = 0;
|
||||
for (const section of sections) {
|
||||
if (deps.inputs.isSectionComplete(section)) {
|
||||
completedCount++;
|
||||
} else if (deps.inputs.isSectionSkipped(section)) {
|
||||
skippedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (completedCount + skippedCount !== sections.length) return undefined;
|
||||
if (completedCount === sections.length) return 'completed';
|
||||
if (skippedCount === sections.length) return 'skipped';
|
||||
return 'mixed';
|
||||
}
|
||||
|
||||
function getStepTelemetryInputs(step: WorkflowSetupStep): WorkflowSetupStepTelemetryInput[] {
|
||||
const inputs: WorkflowSetupStepTelemetryInput[] = [];
|
||||
for (const section of getStepSections(step)) {
|
||||
if (section.credentialType) {
|
||||
inputs.push({
|
||||
input_type: 'credential',
|
||||
node_type: section.node.type,
|
||||
credential_type: section.credentialType,
|
||||
});
|
||||
}
|
||||
|
||||
for (const parameterName of section.parameterNames) {
|
||||
inputs.push({
|
||||
input_type: 'parameter',
|
||||
node_type: section.node.type,
|
||||
parameter_name: parameterName,
|
||||
});
|
||||
}
|
||||
}
|
||||
return inputs;
|
||||
}
|
||||
|
||||
function getStepTelemetryPayload(
|
||||
step: WorkflowSetupStep,
|
||||
outcome?: SetupStepOutcome,
|
||||
): WorkflowSetupStepTelemetryPayload {
|
||||
return {
|
||||
...getSetupTelemetryContext(),
|
||||
request_id: deps.requestId.value,
|
||||
step_index: deps.steps.value.indexOf(step) + 1,
|
||||
step_count: deps.steps.value.length,
|
||||
step_kind: step.kind,
|
||||
setup_inputs: getStepTelemetryInputs(step),
|
||||
...(outcome ? { outcome } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function trackStepShown(step: WorkflowSetupStep): void {
|
||||
const stepKey = getTrackingStepKey(step);
|
||||
if (shownStepKeys.has(stepKey)) return;
|
||||
shownStepKeys.add(stepKey);
|
||||
telemetry.track('Instance AI workflow setup step shown', getStepTelemetryPayload(step));
|
||||
}
|
||||
|
||||
function trackStepHandled(step: WorkflowSetupStep): void {
|
||||
const stepKey = getTrackingStepKey(step);
|
||||
if (!shownStepKeys.has(stepKey) || handledStepKeys.has(stepKey)) return;
|
||||
|
||||
const outcome = getStepOutcome(step);
|
||||
if (!outcome) return;
|
||||
|
||||
handledStepKeys.add(stepKey);
|
||||
telemetry.track(
|
||||
'Instance AI workflow setup step handled',
|
||||
getStepTelemetryPayload(step, outcome),
|
||||
);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => {
|
||||
const step = deps.isReady.value ? deps.activeStep.value : undefined;
|
||||
if (!step) return undefined;
|
||||
|
||||
return {
|
||||
step,
|
||||
key: getTrackingStepKey(step),
|
||||
index: deps.currentStepIndex.value,
|
||||
count: deps.steps.value.length,
|
||||
states: getStepSections(step).map((section) => ({
|
||||
isComplete: deps.inputs.isSectionComplete(section),
|
||||
isSkipped: deps.inputs.isSectionSkipped(section),
|
||||
})),
|
||||
};
|
||||
},
|
||||
(snapshot) => {
|
||||
if (!snapshot) return;
|
||||
trackStepShown(snapshot.step);
|
||||
trackStepHandled(snapshot.step);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function trackSetupInput(): void {
|
||||
const provided: ProvidedSetupInput[] = [];
|
||||
const skipped: SkippedSetupInput[] = [];
|
||||
const explicitlySkipped: SkippedSetupInput[] = [];
|
||||
for (const section of deps.sections.value) {
|
||||
const sectionInputs = getSectionTelemetryInputs(section);
|
||||
if (deps.inputs.isSectionComplete(section)) {
|
||||
provided.push(...sectionInputs);
|
||||
} else {
|
||||
const skippedInputs = sectionInputs.map(toSkippedInput);
|
||||
skipped.push(...skippedInputs);
|
||||
if (deps.inputs.isSectionSkipped(section)) {
|
||||
explicitlySkipped.push(...skippedInputs);
|
||||
}
|
||||
}
|
||||
}
|
||||
telemetry.track('User finished providing input', {
|
||||
...getSetupTelemetryContext(),
|
||||
provided_inputs: provided,
|
||||
skipped_inputs: skipped,
|
||||
explicitly_skipped_inputs: explicitlySkipped,
|
||||
num_tasks: deps.sections.value.length,
|
||||
});
|
||||
}
|
||||
|
||||
function getSectionTelemetryInputs(section: WorkflowSetupSection): ProvidedSetupInput[] {
|
||||
const inputs: ProvidedSetupInput[] = [];
|
||||
if (section.credentialType) {
|
||||
inputs.push({
|
||||
label: getSetupInputLabel(section.node.type, section.credentialType),
|
||||
options: [],
|
||||
option_chosen: 'true',
|
||||
});
|
||||
}
|
||||
for (const parameterName of section.parameterNames) {
|
||||
inputs.push({
|
||||
label: getSetupInputLabel(section.node.type, parameterName),
|
||||
options: [],
|
||||
option_chosen: 'true',
|
||||
});
|
||||
}
|
||||
return inputs;
|
||||
}
|
||||
|
||||
function getSetupInputLabel(nodeType: string, inputName: string): string {
|
||||
return `${nodeType} - ${inputName}`;
|
||||
}
|
||||
|
||||
function toSkippedInput(input: ProvidedSetupInput): SkippedSetupInput {
|
||||
return { label: input.label, options: input.options };
|
||||
}
|
||||
|
||||
return {
|
||||
trackSetupInput,
|
||||
trackStepHandled,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import type {
|
||||
WorkflowSetupGroup,
|
||||
WorkflowSetupSection,
|
||||
WorkflowSetupStep,
|
||||
} from './workflowSetup.types';
|
||||
|
||||
/** Stable section identity used as the key everywhere section state is tracked. */
|
||||
export function buildSectionId(targetNodeName: string, credentialType?: string): string {
|
||||
return `${targetNodeName}:${credentialType ?? 'parameters'}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root + sub-node sections in display order. Centralizes the
|
||||
* optional-root handling so callers don't repeat the same spread.
|
||||
*/
|
||||
export function getGroupSections(group: WorkflowSetupGroup): WorkflowSetupSection[] {
|
||||
return group.rootSection ? [group.rootSection, ...group.subnodeSections] : group.subnodeSections;
|
||||
}
|
||||
|
||||
/** Returns every section a step represents (one for section steps, root+subnodes for group steps). */
|
||||
export function getStepSections(step: WorkflowSetupStep): WorkflowSetupSection[] {
|
||||
return step.kind === 'group' ? getGroupSections(step.group) : [step.section];
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import type { InstanceAiWorkflowSetupNode } from '@n8n/api-types';
|
||||
import type { INodeParameters } from 'n8n-workflow';
|
||||
|
||||
/**
|
||||
* One form unit (per `node × credential-or-parameters`).
|
||||
* Backs all input/skip/payload state.
|
||||
*/
|
||||
export interface WorkflowSetupSection {
|
||||
id: string;
|
||||
credentialType?: string;
|
||||
targetNodeName: string;
|
||||
node: InstanceAiWorkflowSetupNode['node'];
|
||||
currentCredentialId: string | null;
|
||||
parameterNames: string[];
|
||||
credentialTargetNodes: Array<{ id: string; name: string; type: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Root node + its setup/sub-node sections grouped under a single wizard step.
|
||||
*
|
||||
* `rootSection` is optional because the root node may not have a setup
|
||||
* request of its own — its sub-nodes can carry all the configuration. When
|
||||
* the root node does have a setup request, credentials and parameters from
|
||||
* that request are bundled into a single section.
|
||||
*/
|
||||
export interface WorkflowSetupGroup {
|
||||
subnodeRootNode: { name: string; type: string; typeVersion: number; id: string };
|
||||
rootSection?: WorkflowSetupSection;
|
||||
subnodeSections: WorkflowSetupSection[];
|
||||
}
|
||||
|
||||
/**
|
||||
* What the wizard navigates: either a single section or a grouped step.
|
||||
*/
|
||||
export type WorkflowSetupStep =
|
||||
| { kind: 'section'; section: WorkflowSetupSection }
|
||||
| { kind: 'group'; group: WorkflowSetupGroup };
|
||||
|
||||
export interface WorkflowSetupApplyPayload {
|
||||
nodeCredentials?: Record<string, Record<string, string>>;
|
||||
nodeParameters?: Record<string, INodeParameters>;
|
||||
}
|
||||
|
||||
export type TerminalState = 'applying' | 'applied' | 'partial' | 'deferred';
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
import type { INodeUi } from '@/Interface';
|
||||
import type { INodeProperties, INodeTypeDescription } from 'n8n-workflow';
|
||||
import { NodeHelpers } from 'n8n-workflow';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { getWorkflowSetupParameterIssues } from './workflowSetupParameterIssues';
|
||||
|
||||
function makeNode(overrides: Partial<INodeUi> = {}): INodeUi {
|
||||
return {
|
||||
id: 'http-request',
|
||||
name: 'HTTP Request',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4.2,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
...overrides,
|
||||
} as INodeUi;
|
||||
}
|
||||
|
||||
function makeNodeType(properties: INodeProperties[]): INodeTypeDescription {
|
||||
return {
|
||||
displayName: 'HTTP Request',
|
||||
name: 'n8n-nodes-base.httpRequest',
|
||||
group: ['transform'],
|
||||
version: 4.2,
|
||||
description: 'Makes HTTP requests',
|
||||
defaults: { name: 'HTTP Request' },
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties,
|
||||
} as INodeTypeDescription;
|
||||
}
|
||||
|
||||
describe('getWorkflowSetupParameterIssues', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('returns no issues without a node type or requested parameters', () => {
|
||||
const getNodeParametersIssuesSpy = vi.spyOn(NodeHelpers, 'getNodeParametersIssues');
|
||||
const node = makeNode();
|
||||
const nodeType = makeNodeType([
|
||||
{ displayName: 'URL', name: 'url', type: 'string', default: '' },
|
||||
]);
|
||||
|
||||
expect(getWorkflowSetupParameterIssues(node, null, ['url'])).toEqual({});
|
||||
expect(getWorkflowSetupParameterIssues(node, nodeType, [])).toEqual({});
|
||||
expect(getNodeParametersIssuesSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns validation issues only for requested visible parameters', () => {
|
||||
vi.spyOn(NodeHelpers, 'getNodeParametersIssues').mockReturnValue({
|
||||
parameters: {
|
||||
url: ['URL is required'],
|
||||
method: ['Method is required'],
|
||||
ignored: ['Ignored issue'],
|
||||
},
|
||||
});
|
||||
const node = makeNode({ parameters: { url: '', method: '' } });
|
||||
const nodeType = makeNodeType([
|
||||
{ displayName: 'URL', name: 'url', type: 'string', default: '' },
|
||||
{ displayName: 'Method', name: 'method', type: 'string', default: '' },
|
||||
{ displayName: 'Ignored', name: 'ignored', type: 'string', default: '' },
|
||||
]);
|
||||
|
||||
expect(getWorkflowSetupParameterIssues(node, nodeType, ['url', 'method'])).toEqual({
|
||||
url: ['URL is required'],
|
||||
method: ['Method is required'],
|
||||
});
|
||||
});
|
||||
|
||||
it('adds placeholder issues for requested visible parameters', () => {
|
||||
vi.spyOn(NodeHelpers, 'getNodeParametersIssues').mockReturnValue({ parameters: {} });
|
||||
const node = makeNode({ parameters: { url: '<__PLACEHOLDER_VALUE__Enter URL__>' } });
|
||||
const nodeType = makeNodeType([
|
||||
{ displayName: 'URL', name: 'url', type: 'string', default: '' },
|
||||
]);
|
||||
|
||||
expect(getWorkflowSetupParameterIssues(node, nodeType, ['url'])).toEqual({
|
||||
url: ['Placeholder "Enter URL" - please provide the real value'],
|
||||
});
|
||||
});
|
||||
|
||||
it('skips hidden parameters and parameters hidden by display options', () => {
|
||||
vi.spyOn(NodeHelpers, 'getNodeParametersIssues').mockReturnValue({
|
||||
parameters: {
|
||||
hidden: ['Hidden is required'],
|
||||
conditional: ['Conditional is required'],
|
||||
},
|
||||
});
|
||||
const displayParameterSpy = vi.spyOn(NodeHelpers, 'displayParameter').mockReturnValue(false);
|
||||
const node = makeNode({ parameters: { hidden: '', conditional: '' } });
|
||||
const conditionalProperty: INodeProperties = {
|
||||
displayName: 'Conditional',
|
||||
name: 'conditional',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: { show: { mode: ['advanced'] } },
|
||||
};
|
||||
const nodeType = makeNodeType([
|
||||
{ displayName: 'Hidden', name: 'hidden', type: 'hidden', default: '' },
|
||||
conditionalProperty,
|
||||
]);
|
||||
|
||||
expect(getWorkflowSetupParameterIssues(node, nodeType, ['hidden', 'conditional'])).toEqual({});
|
||||
expect(displayParameterSpy).toHaveBeenCalledWith(
|
||||
node.parameters,
|
||||
conditionalProperty,
|
||||
node,
|
||||
nodeType,
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps issues when any duplicate parameter definition is visible', () => {
|
||||
vi.spyOn(NodeHelpers, 'getNodeParametersIssues').mockReturnValue({
|
||||
parameters: { value: ['Value is required'] },
|
||||
});
|
||||
const hiddenDefinition: INodeProperties = {
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: { show: { mode: ['advanced'] } },
|
||||
};
|
||||
const visibleDefinition: INodeProperties = {
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: { show: { mode: ['basic'] } },
|
||||
};
|
||||
vi.spyOn(NodeHelpers, 'displayParameter').mockImplementation((_parameters, property) => {
|
||||
return property === visibleDefinition;
|
||||
});
|
||||
const node = makeNode({ parameters: { value: '' } });
|
||||
const nodeType = makeNodeType([hiddenDefinition, visibleDefinition]);
|
||||
|
||||
expect(getWorkflowSetupParameterIssues(node, nodeType, ['value'])).toEqual({
|
||||
value: ['Value is required'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import { findPlaceholderDetails } from '@n8n/utils';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import type { INodeParameters, INodeProperties, INodeTypeDescription } from 'n8n-workflow';
|
||||
import { NodeHelpers } from 'n8n-workflow';
|
||||
|
||||
export function getWorkflowSetupParameterIssues(
|
||||
node: INodeUi,
|
||||
nodeType: INodeTypeDescription | null,
|
||||
parameterNames: string[],
|
||||
): Record<string, string[]> {
|
||||
if (!nodeType || parameterNames.length === 0) return {};
|
||||
|
||||
const wanted = new Set(parameterNames);
|
||||
const allIssues =
|
||||
NodeHelpers.getNodeParametersIssues(nodeType.properties, node, nodeType)?.parameters ?? {};
|
||||
// A parameter name can have multiple definitions with different displayOptions.
|
||||
// Keep all variants so issues are shown when any matching definition is visible.
|
||||
const parameterDefinitionsByName = groupParameterDefinitionsByName(nodeType.properties);
|
||||
const filtered: Record<string, string[]> = {};
|
||||
|
||||
for (const name of wanted) {
|
||||
const parameterDefinitions = parameterDefinitionsByName.get(name);
|
||||
if (
|
||||
!parameterDefinitions ||
|
||||
!parameterDefinitions.some((definition) =>
|
||||
isParameterDefinitionVisible(node.parameters, definition, node, nodeType),
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const issues = [...(allIssues[name] ?? [])];
|
||||
const placeholderDetails = findPlaceholderDetails(node.parameters[name]);
|
||||
if (placeholderDetails.length > 0) {
|
||||
issues.push(`Placeholder "${placeholderDetails[0].label}" - please provide the real value`);
|
||||
}
|
||||
|
||||
if (issues.length > 0) filtered[name] = issues;
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function groupParameterDefinitionsByName(
|
||||
properties: INodeProperties[],
|
||||
): Map<string, INodeProperties[]> {
|
||||
const result = new Map<string, INodeProperties[]>();
|
||||
for (const prop of properties) {
|
||||
const existing = result.get(prop.name);
|
||||
if (existing) {
|
||||
existing.push(prop);
|
||||
} else {
|
||||
result.set(prop.name, [prop]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function isParameterDefinitionVisible(
|
||||
parameters: INodeParameters,
|
||||
property: INodeProperties,
|
||||
node: INodeUi,
|
||||
nodeType: INodeTypeDescription,
|
||||
): boolean {
|
||||
if (property.type === 'hidden') return false;
|
||||
if (!property.displayOptions) return true;
|
||||
return NodeHelpers.displayParameter(parameters, property, node, nodeType);
|
||||
}
|
||||
|
|
@ -131,6 +131,9 @@ const workflowDocumentStore = injectWorkflowDocumentStore();
|
|||
const contextNode = computed<INode | null>(() => {
|
||||
if (ndvStore.activeNode) return ndvStore.activeNode;
|
||||
const modalState = uiStore.modalsById[CREDENTIAL_EDIT_MODAL_KEY];
|
||||
if (isCredentialModalState(modalState) && modalState.contextNode) {
|
||||
return modalState.contextNode;
|
||||
}
|
||||
const fallbackName = isCredentialModalState(modalState) ? modalState.nodeName : undefined;
|
||||
return fallbackName ? (workflowDocumentStore.value?.getNodeByName(fallbackName) ?? null) : null;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -254,7 +254,8 @@ describe('NodeCredentials', () => {
|
|||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
httpNode.name,
|
||||
httpNode,
|
||||
{ hideAskAssistant: false },
|
||||
);
|
||||
});
|
||||
|
|
@ -673,7 +674,7 @@ describe('NodeCredentials', () => {
|
|||
|
||||
await userEvent.click(screen.getByTestId('setup-manually-link'));
|
||||
|
||||
// createNewCredential calls openNewCredential with (type, showAuthOptions, forceManualMode, projectId)
|
||||
// createNewCredential calls openNewCredential with context for auth-option resolution
|
||||
// "setup manually" passes forceManualMode=true
|
||||
expect(uiStore.openNewCredential).toHaveBeenCalledWith(
|
||||
'slackOAuth2Api',
|
||||
|
|
@ -681,7 +682,12 @@ describe('NodeCredentials', () => {
|
|||
true,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
slackNode.name,
|
||||
expect.objectContaining({
|
||||
id: slackNode.id,
|
||||
name: slackNode.name,
|
||||
type: slackNode.type,
|
||||
}),
|
||||
{ hideAskAssistant: false },
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -396,7 +396,8 @@ function createNewCredential(
|
|||
forceManualMode,
|
||||
props.projectId,
|
||||
props.suggestedCredentialName,
|
||||
undefined,
|
||||
props.node.name,
|
||||
props.node,
|
||||
{ hideAskAssistant: props.hideAskAssistant },
|
||||
);
|
||||
telemetry.track('User opened Credential modal', {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
import { useCredentialsStore } from '../../credentials.store';
|
||||
import { useCredentialTestInBackground } from '../useCredentialTestInBackground';
|
||||
|
||||
describe('useCredentialTestInBackground', () => {
|
||||
let credentialsStore: ReturnType<typeof mockedStore<typeof useCredentialsStore>>;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }));
|
||||
credentialsStore = mockedStore(useCredentialsStore);
|
||||
credentialsStore.credentialTestResults = new Map();
|
||||
});
|
||||
|
||||
describe('hydrateCredentialTestResults', () => {
|
||||
it('writes success and error entries to the credentials store', () => {
|
||||
const { hydrateCredentialTestResults } = useCredentialTestInBackground();
|
||||
|
||||
hydrateCredentialTestResults([
|
||||
{ id: 'cred-ok', success: true },
|
||||
{ id: 'cred-bad', success: false },
|
||||
]);
|
||||
|
||||
expect(credentialsStore.credentialTestResults.get('cred-ok')).toBe('success');
|
||||
expect(credentialsStore.credentialTestResults.get('cred-bad')).toBe('error');
|
||||
});
|
||||
|
||||
it('overwrites existing entries when re-hydrating with a new result', () => {
|
||||
credentialsStore.credentialTestResults.set('cred-1', 'error');
|
||||
const { hydrateCredentialTestResults } = useCredentialTestInBackground();
|
||||
|
||||
hydrateCredentialTestResults([{ id: 'cred-1', success: true }]);
|
||||
|
||||
expect(credentialsStore.credentialTestResults.get('cred-1')).toBe('success');
|
||||
});
|
||||
|
||||
it('is a no-op for an empty list', () => {
|
||||
const { hydrateCredentialTestResults } = useCredentialTestInBackground();
|
||||
|
||||
hydrateCredentialTestResults([]);
|
||||
|
||||
expect(credentialsStore.credentialTestResults.size).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
|
||||
import { useCredentialsStore } from '@/features/credentials/credentials.store';
|
||||
|
||||
export function useCredentialTestInBackground() {
|
||||
const credentialsStore = useCredentialsStore();
|
||||
|
||||
/**
|
||||
* Checks whether a credential type has a test mechanism defined.
|
||||
* Returns true if either the credential type itself defines a `test` block
|
||||
* or any node with access declares `testedBy` for it.
|
||||
*/
|
||||
const isCredentialTypeTestable = (credentialTypeName: string): boolean => {
|
||||
const credType = credentialsStore.getCredentialTypeByName(credentialTypeName);
|
||||
if (credType?.test) return true;
|
||||
|
||||
const nodesWithAccess = credentialsStore.getNodesWithAccess(credentialTypeName);
|
||||
return nodesWithAccess.some((node) =>
|
||||
node.credentials?.some((cred) => cred.name === credentialTypeName && cred.testedBy),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests a saved credential in the background.
|
||||
* Fetches the credential's redacted data first so the backend can unredact and test.
|
||||
* Skips if the credential is already tested OK or has a test in flight.
|
||||
* The result is tracked automatically in the credentials store as a side effect of testCredential.
|
||||
*/
|
||||
async function testCredentialInBackground(
|
||||
credentialId: string,
|
||||
credentialName: string,
|
||||
credentialType: string,
|
||||
) {
|
||||
if (!isCredentialTypeTestable(credentialType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
credentialsStore.isCredentialTestedOk(credentialId) ||
|
||||
credentialsStore.isCredentialTestPending(credentialId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const credentialResponse = await credentialsStore.getCredentialData({ id: credentialId });
|
||||
if (!credentialResponse?.data || typeof credentialResponse.data === 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Re-check after the async fetch — another caller (e.g. CredentialEdit) may have
|
||||
// started or completed a test while we were fetching credential data.
|
||||
if (
|
||||
credentialsStore.isCredentialTestedOk(credentialId) ||
|
||||
credentialsStore.isCredentialTestPending(credentialId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { ownedBy, sharedWithProjects, oauthTokenData, ...data } = credentialResponse.data;
|
||||
|
||||
// OAuth credentials can't be tested via the API — the presence of token data
|
||||
// means the OAuth flow completed successfully, which is the equivalent of a passing test.
|
||||
if (oauthTokenData) {
|
||||
credentialsStore.credentialTestResults.set(credentialId, 'success');
|
||||
return;
|
||||
}
|
||||
|
||||
await credentialsStore.testCredential({
|
||||
id: credentialId,
|
||||
name: credentialName,
|
||||
type: credentialType,
|
||||
data: data as ICredentialDataDecryptedObject,
|
||||
});
|
||||
} catch {
|
||||
// Test failure is tracked in the store as a side effect
|
||||
}
|
||||
}
|
||||
|
||||
function hydrateCredentialTestResults(results: Array<{ id: string; success: boolean }>) {
|
||||
for (const { id, success } of results) {
|
||||
credentialsStore.credentialTestResults.set(id, success ? 'success' : 'error');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isCredentialTypeTestable,
|
||||
testCredentialInBackground,
|
||||
hydrateCredentialTestResults,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,11 +1,7 @@
|
|||
import { computed, ref, watch, type Ref } from 'vue';
|
||||
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import {
|
||||
type ICredentialDataDecryptedObject,
|
||||
type INode,
|
||||
isResourceLocatorValue,
|
||||
} from 'n8n-workflow';
|
||||
import { type INode, isResourceLocatorValue } from 'n8n-workflow';
|
||||
import type { SetupCardItem, NodeSetupState } from '@/features/setupPanel/setupPanel.types';
|
||||
import { isCardComplete } from '@/features/setupPanel/setupPanel.utils';
|
||||
|
||||
|
|
@ -39,6 +35,7 @@ import { sortNodesByExecutionOrder } from '@/app/utils/workflowUtils';
|
|||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import { useTemplatesStore } from '@/features/workflows/templates/templates.store';
|
||||
import { groupSetupCards } from '@/features/setupPanel/composables/groupSetupCards';
|
||||
import { useCredentialTestInBackground } from '@/features/credentials/composables/useCredentialTestInBackground';
|
||||
|
||||
/**
|
||||
* Composable that manages workflow setup state for credential configuration.
|
||||
|
|
@ -60,6 +57,7 @@ export const useWorkflowSetupState = (
|
|||
const nodeHelpers = useNodeHelpers();
|
||||
const environmentsStore = useEnvironmentsStore();
|
||||
const templatesStore = useTemplatesStore();
|
||||
const { isCredentialTypeTestable, testCredentialInBackground } = useCredentialTestInBackground();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
|
||||
const sourceNodes = computed(() => nodes?.value ?? workflowDocumentStore.value.allNodes);
|
||||
|
|
@ -226,22 +224,6 @@ export const useWorkflowSetupState = (
|
|||
return credentialTypeInfo?.displayName ?? credentialType;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks whether a credential type has a test mechanism defined.
|
||||
* Returns true if either the credential type itself defines a `test` block
|
||||
* or any node with access declares `testedBy` for it.
|
||||
* Non-testable types (e.g. Header Auth) are considered complete when just set.
|
||||
*/
|
||||
const isCredentialTypeTestable = (credentialTypeName: string): boolean => {
|
||||
const credType = credentialsStore.getCredentialTypeByName(credentialTypeName);
|
||||
if (credType?.test) return true;
|
||||
|
||||
const nodesWithAccess = credentialsStore.getNodesWithAccess(credentialTypeName);
|
||||
return nodesWithAccess.some((node) =>
|
||||
node.credentials?.some((cred) => cred.name === credentialTypeName && cred.testedBy),
|
||||
);
|
||||
};
|
||||
|
||||
const isTriggerNode = (node: INodeUi): boolean => {
|
||||
return nodeTypesStore.isTriggerNode(node.type);
|
||||
};
|
||||
|
|
@ -703,63 +685,6 @@ export const useWorkflowSetupState = (
|
|||
return setupCards.value.length > 0 && setupCards.value.every((card) => isCardComplete(card));
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests a saved credential in the background.
|
||||
* Fetches the credential's redacted data first so the backend can unredact and test.
|
||||
* Skips if the credential is already tested OK or has a test in flight.
|
||||
* The result is tracked automatically in the credentials store as a side effect of testCredential.
|
||||
*/
|
||||
async function testCredentialInBackground(
|
||||
credentialId: string,
|
||||
credentialName: string,
|
||||
credentialType: string,
|
||||
) {
|
||||
if (!isCredentialTypeTestable(credentialType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
credentialsStore.isCredentialTestedOk(credentialId) ||
|
||||
credentialsStore.isCredentialTestPending(credentialId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const credentialResponse = await credentialsStore.getCredentialData({ id: credentialId });
|
||||
if (!credentialResponse?.data || typeof credentialResponse.data === 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Re-check after the async fetch — another caller (e.g. CredentialEdit) may have
|
||||
// started or completed a test while we were fetching credential data.
|
||||
if (
|
||||
credentialsStore.isCredentialTestedOk(credentialId) ||
|
||||
credentialsStore.isCredentialTestPending(credentialId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { ownedBy, sharedWithProjects, oauthTokenData, ...data } = credentialResponse.data;
|
||||
|
||||
// OAuth credentials can't be tested via the API — the presence of token data
|
||||
// means the OAuth flow completed successfully, which is the equivalent of a passing test.
|
||||
if (oauthTokenData) {
|
||||
credentialsStore.credentialTestResults.set(credentialId, 'success');
|
||||
return;
|
||||
}
|
||||
|
||||
await credentialsStore.testCredential({
|
||||
id: credentialId,
|
||||
name: credentialName,
|
||||
type: credentialType,
|
||||
data: data as ICredentialDataDecryptedObject,
|
||||
});
|
||||
} catch {
|
||||
// Test failure is tracked in the store as a side effect
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the node names affected by a credential operation.
|
||||
* Checks credentialTypeStates first (grouped cards), then falls back to
|
||||
|
|
|
|||
|
|
@ -825,6 +825,65 @@ describe('setupPanel.utils', () => {
|
|||
expect(issues).toHaveProperty('event');
|
||||
});
|
||||
|
||||
it('should detect required parameter issues when a controlling default has same-named siblings', () => {
|
||||
// Mirrors OpenAI v2's shape: a single `resource` parameter controls
|
||||
// which `operation` (and which `modelId`) is displayed. Naively
|
||||
// filling defaults picks the first `operation` regardless of resource.
|
||||
const nodeType = {
|
||||
properties: [
|
||||
createTestNodeProperties({
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
default: 'text',
|
||||
options: [
|
||||
{ name: 'Audio', value: 'audio' },
|
||||
{ name: 'Text', value: 'text' },
|
||||
],
|
||||
}),
|
||||
createTestNodeProperties({
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
default: 'generate',
|
||||
displayOptions: { show: { resource: ['audio'] } },
|
||||
}),
|
||||
createTestNodeProperties({
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
default: 'response',
|
||||
displayOptions: { show: { resource: ['text'] } },
|
||||
}),
|
||||
createTestNodeProperties({
|
||||
displayName: 'Model',
|
||||
name: 'modelId',
|
||||
type: 'resourceLocator',
|
||||
required: true,
|
||||
default: { mode: 'list', value: '' },
|
||||
modes: [
|
||||
{ displayName: 'From List', name: 'list', type: 'list' },
|
||||
{ displayName: 'ID', name: 'id', type: 'string' },
|
||||
],
|
||||
displayOptions: { show: { resource: ['text'], operation: ['response'] } },
|
||||
}),
|
||||
],
|
||||
} as unknown as INodeTypeDescription;
|
||||
|
||||
mockNodeTypeProvider.getNodeType.mockReturnValue(nodeType);
|
||||
|
||||
const node = createTestNode({
|
||||
type: '@n8n/n8n-nodes-langchain.openAi',
|
||||
parameters: {
|
||||
modelId: { __rl: true, value: '', mode: 'id' },
|
||||
},
|
||||
});
|
||||
|
||||
const issues = getNodeParametersIssues(mockNodeTypeProvider, node);
|
||||
|
||||
expect(issues).toHaveProperty('modelId');
|
||||
});
|
||||
|
||||
it('should not include issues for parameter variants that are not displayed', () => {
|
||||
const nodeType = {
|
||||
properties: [
|
||||
|
|
|
|||
|
|
@ -54,13 +54,20 @@ export function getNodeParametersIssues(nodeTypesStore: NodeTypeProvider, node:
|
|||
// Fill in default values for parameters not explicitly set on the node.
|
||||
// Required parameters with valid defaults (e.g. binaryPropertyName: 'data')
|
||||
// are not stored in node.parameters when the user hasn't changed them.
|
||||
// Without this, the issue checker flags them as missing.
|
||||
const paramsWithDefaults: INodeParameters = { ...node.parameters };
|
||||
for (const prop of nodeType.properties) {
|
||||
if (!(prop.name in paramsWithDefaults) && prop.default !== undefined) {
|
||||
paramsWithDefaults[prop.name] = prop.default;
|
||||
}
|
||||
}
|
||||
// Without this, the issue checker flags them as missing. We delegate to
|
||||
// `getNodeParameters` so duplicate property names gated by sibling values
|
||||
// (e.g. OpenAI v2's per-resource `operation`) resolve to the displayed
|
||||
// variant rather than whichever appears first.
|
||||
const resolved =
|
||||
NodeHelpers.getNodeParameters(
|
||||
nodeType.properties,
|
||||
node.parameters,
|
||||
true,
|
||||
false,
|
||||
node,
|
||||
nodeType,
|
||||
) ?? node.parameters;
|
||||
const paramsWithDefaults: INodeParameters = resolved;
|
||||
|
||||
const nodeWithDefaults: INode = { ...node, parameters: paramsWithDefaults };
|
||||
const issues = NodeHelpers.getNodeParametersIssues(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "REGEX",
|
||||
"regex": "[\\s\\S]*\\[\\{\"type\":\"text\",\"text\":\"You are the n8n Instance Agent — an AI assistant embedde[\\s\\S]*Set up the workflow named \\\\\"B3 Full Wizard Apply\\\\\"\\.[\\s\\S]*"
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"x-envoy-upstream-service-time": [
|
||||
"1537"
|
||||
],
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"traceresponse": [
|
||||
"00-7b6848ead6ef5299b7c92d8647b466a9-f2c7a00c5b42fbf1-01"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"set-cookie": [
|
||||
"_cfuvid=MBWXL_g7CBwGNs50BPJBIaysg9mWasTGrLZPFtzgigU-1777974532.0862312-1.0.1.1-HY1cG9pPyykspwrSUnBkELqKKkO.v.ZlF97l1SzvQrU; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CajDGBCfXLU1Zk33EGnAW"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-05-05T09:48:52Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"26975000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"27000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-05-05T09:48:52Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19998"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-05-05T09:48:52Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-05-05T09:48:52Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"22475000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"22500000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Tue, 05 May 2026 09:48:53 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"9f6ed4f98ebfbda6-SOF"
|
||||
]
|
||||
},
|
||||
"cookies": {
|
||||
"_cfuvid": "MBWXL_g7CBwGNs50BPJBIaysg9mWasTGrLZPFtzgigU-1777974532.0862312-1.0.1.1-HY1cG9pPyykspwrSUnBkELqKKkO.v.ZlF97l1SzvQrU"
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_01DA2PqfvA5THfKrLKuUZ7oD\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":337,\"cache_creation_input_tokens\":13705,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":13705,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":45,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_012sbwda3uPiXbX9AbYTKP8D\",\"name\":\"workflows\",\"input\":{},\"caller\":{\"type\":\"direct\"}} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"action\\\": \\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"list\\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\", \\\"q\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"uery\\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\": \\\"B3 F\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"ull Wizard \"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"Apply\\\"}\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":337,\"cache_creation_input_tokens\":13705,\"cache_read_input_tokens\":0,\"output_tokens\":75} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxREEyUHFmdkE1VEhmS3JMS3VVWjdvRCIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjMzNywiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjoxMzcwNSwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjEzNzA1LCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6NDUsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIiwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCJ9fSAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdGFydApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0YXJ0IiwiaW5kZXgiOjAsImNvbnRlbnRfYmxvY2siOnsidHlwZSI6InRvb2xfdXNlIiwiaWQiOiJ0b29sdV8wMTJzYndkYTN1UGlYYlg5QWJZVEtQOEQiLCJuYW1lIjoid29ya2Zsb3dzIiwiaW5wdXQiOnt9LCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9fSAgICAgICAgIH0KCmV2ZW50OiBwaW5nCmRhdGE6IHsidHlwZSI6ICJwaW5nIn0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiIifSAgICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IntcImFjdGlvblwiOiBcIiJ9ICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoibGlzdFwiIn0gICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiLCBcInEifSAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJ1ZXJ5XCIifSAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiI6IFwiQjMgRiJ9IH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJ1bGwgV2l6YXJkICJ9ICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IkFwcGx5XCJ9In0gICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfc3RvcApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0b3AiLCJpbmRleCI6MCAgICAgICAgfQoKZXZlbnQ6IG1lc3NhZ2VfZGVsdGEKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9kZWx0YSIsImRlbHRhIjp7InN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJzdG9wX2RldGFpbHMiOm51bGx9LCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjMzNywiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjoxMzcwNSwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsIm91dHB1dF90b2tlbnMiOjc1fSAgICAgIH0KCmV2ZW50OiBtZXNzYWdlX3N0b3AKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdG9wIiB9Cgo=",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "0000-1777974540866-unknown-host-POST-_v1_messages-abc49c4f.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "REGEX",
|
||||
"regex": "[\\s\\S]*\\[\\{\"type\":\"text\",\"text\":\"You are the n8n Instance Agent — an AI assistant embedde[\\s\\S]*\"type\"\\s*:\\s*\"tool_use\"[\\s\\S]{0,300}\"name\"\\s*:\\s*\"workflows\"[\\s\\S]{0,500}\"action\"\\s*:\\s*\"list\"[\\s\\S]*"
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"x-envoy-upstream-service-time": [
|
||||
"1158"
|
||||
],
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"traceresponse": [
|
||||
"00-ecd5ea4d95aa8ba662ef9b7a504d11ec-f600a43185e6a636-01"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"set-cookie": [
|
||||
"_cfuvid=XDew3.HSrrls1SiRYRK1WKQULHmprCKZSXH7EdLYsZM-1777974534.2739832-1.0.1.1-ortTOOCCp0oe6wj0TwrvU5aPn5bpVWPIu0gRjQfKFuU; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CajDGLZUvRhvUR6SYgZLq"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-05-05T09:48:54Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"26975000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"27000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-05-05T09:48:54Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19998"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-05-05T09:48:54Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-05-05T09:48:54Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"22475000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"22500000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Tue, 05 May 2026 09:48:55 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"9f6ed507383b4d27-SOF"
|
||||
]
|
||||
},
|
||||
"cookies": {
|
||||
"_cfuvid": "XDew3.HSrrls1SiRYRK1WKQULHmprCKZSXH7EdLYsZM-1777974534.2739832-1.0.1.1-ortTOOCCp0oe6wj0TwrvU5aPn5bpVWPIu0gRjQfKFuU"
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_0154YYfT5XLEVhvpAuFUgPYS\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":118,\"cache_creation_input_tokens\":412,\"cache_read_input_tokens\":13705,\"cache_creation\":{\"ephemeral_5m_input_tokens\":412,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":58,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_014sXMa88NbSX5wTn3Y5CLuK\",\"name\":\"workflows\",\"input\":{},\"caller\":{\"type\":\"direct\"}} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"actio\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"n\\\": \\\"s\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"et\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"up\\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\", \\\"workf\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"lowId\\\": \\\"T\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"qaMpj2\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"sVOinXnc2\\\"}\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":118,\"cache_creation_input_tokens\":412,\"cache_read_input_tokens\":13705,\"output_tokens\":84} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxNTRZWWZUNVhMRVZodnBBdUZVZ1BZUyIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjExOCwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjo0MTIsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjoxMzcwNSwiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6NDEyLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6NTgsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIiwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCJ9fSAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfc3RhcnQKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdGFydCIsImluZGV4IjowLCJjb250ZW50X2Jsb2NrIjp7InR5cGUiOiJ0b29sX3VzZSIsImlkIjoidG9vbHVfMDE0c1hNYTg4TmJTWDV3VG4zWTVDTHVLIiwibmFtZSI6IndvcmtmbG93cyIsImlucHV0Ijp7fSwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifX0gICAgIH0KCmV2ZW50OiBwaW5nCmRhdGE6IHsidHlwZSI6ICJwaW5nIn0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiIifSB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoie1wiYWN0aW8ifX0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJuXCI6IFwicyJ9ICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiZXQifSAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoidXBcIiJ9ICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiLCBcIndvcmtmIn0gICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoibG93SWRcIjogXCJUIn0gICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoicWFNcGoyIn0gICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoic1ZPaW5YbmMyXCJ9In0gICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdG9wCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RvcCIsImluZGV4IjowIH0KCmV2ZW50OiBtZXNzYWdlX2RlbHRhCmRhdGE6IHsidHlwZSI6Im1lc3NhZ2VfZGVsdGEiLCJkZWx0YSI6eyJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsfSwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjoxMTgsImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6NDEyLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MTM3MDUsIm91dHB1dF90b2tlbnMiOjg0fSAgICAgIH0KCmV2ZW50OiBtZXNzYWdlX3N0b3AKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdG9wIiAgICAgICAgICB9Cgo=",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "0001-1777974540867-unknown-host-POST-_v1_messages-c17e02f9.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "REGEX",
|
||||
"regex": "[\\s\\S]*\\[\\{\"type\":\"text\",\"text\":\"You are the n8n Instance Agent — an AI assistant embedde[\\s\\S]*\"type\"\\s*:\\s*\"tool_use\"[\\s\\S]{0,300}\"name\"\\s*:\\s*\"workflows\"[\\s\\S]{0,500}\"action\"\\s*:\\s*\"setup\"[\\s\\S]*"
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"x-envoy-upstream-service-time": [
|
||||
"700"
|
||||
],
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"traceresponse": [
|
||||
"00-2ac185af1f347fae7ab105c38dbaabb7-54ca64405ec77f42-01"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"set-cookie": [
|
||||
"_cfuvid=Jd8ptkQ9X5Ym.YDeRPTnxb4AKwlyzj3Y_vWPCCrk4e0-1777974537.9154794-1.0.1.1-7_1kJLhFqe3Ilrhve0tfVLyjgCiVx7l9MvmovIEhVHI; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CajDGc8rScY2mmHZa3yvq"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-05-05T09:48:58Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"26975000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"27000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-05-05T09:48:58Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19998"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-05-05T09:48:58Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-05-05T09:48:58Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"22475000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"22500000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Tue, 05 May 2026 09:48:58 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"9f6ed51dfef23134-SOF"
|
||||
]
|
||||
},
|
||||
"cookies": {
|
||||
"_cfuvid": "Jd8ptkQ9X5Ym.YDeRPTnxb4AKwlyzj3Y_vWPCCrk4e0-1777974537.9154794-1.0.1.1-7_1kJLhFqe3Ilrhve0tfVLyjgCiVx7l9MvmovIEhVHI"
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_01G8T58XDzfc5iQ6xxVo58yX\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":439,\"cache_creation_input_tokens\":412,\"cache_read_input_tokens\":13705,\"cache_creation\":{\"ephemeral_5m_input_tokens\":412,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"The\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" \\\"B3 Full Wizard Apply\\\" workflow has been set up successfully. Here's what was configured:\\n\\n- **HTTP\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" Request node** — pointed to `https://example.com/api` using the **B3 Parameter Header Auth**\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" credential for authentication.\\n\\nThe workflow is saved and ready. Let me know if you'd like to\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" test it or make any changes.\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":439,\"cache_creation_input_tokens\":412,\"cache_read_input_tokens\":13705,\"output_tokens\":82} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxRzhUNThYRHpmYzVpUTZ4eFZvNTh5WCIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjQzOSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjo0MTIsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjoxMzcwNSwiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6NDEyLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6MSwic2VydmljZV90aWVyIjoic3RhbmRhcmQiLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIn19ICAgICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfc3RhcnQKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdGFydCIsImluZGV4IjowLCJjb250ZW50X2Jsb2NrIjp7InR5cGUiOiJ0ZXh0IiwidGV4dCI6IiJ9ICAgICAgICAgICAgICB9CgpldmVudDogcGluZwpkYXRhOiB7InR5cGUiOiAicGluZyJ9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IlRoZSJ9ICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgXCJCMyBGdWxsIFdpemFyZCBBcHBseVwiIHdvcmtmbG93IGhhcyBiZWVuIHNldCB1cCBzdWNjZXNzZnVsbHkuIEhlcmUncyB3aGF0IHdhcyBjb25maWd1cmVkOlxuXG4tICoqSFRUUCJ9ICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiBSZXF1ZXN0IG5vZGUqKiDigJQgcG9pbnRlZCB0byBgaHR0cHM6Ly9leGFtcGxlLmNvbS9hcGlgIHVzaW5nIHRoZSAqKkIzIFBhcmFtZXRlciBIZWFkZXIgQXV0aCoqIn0gICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgY3JlZGVudGlhbCBmb3IgYXV0aGVudGljYXRpb24uXG5cblRoZSB3b3JrZmxvdyBpcyBzYXZlZCBhbmQgcmVhZHkuIExldCBtZSBrbm93IGlmIHlvdSdkIGxpa2UgdG8ifSAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgdGVzdCBpdCBvciBtYWtlIGFueSBjaGFuZ2VzLiJ9ICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjAgICAgfQoKZXZlbnQ6IG1lc3NhZ2VfZGVsdGEKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9kZWx0YSIsImRlbHRhIjp7InN0b3BfcmVhc29uIjoiZW5kX3R1cm4iLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJzdG9wX2RldGFpbHMiOm51bGx9LCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjQzOSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjo0MTIsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjoxMzcwNSwib3V0cHV0X3Rva2VucyI6ODJ9ICB9CgpldmVudDogbWVzc2FnZV9zdG9wCmRhdGE6IHsidHlwZSI6Im1lc3NhZ2Vfc3RvcCIgICAgICB9Cgo=",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "0002-1777974540867-unknown-host-POST-_v1_messages-97aea00c.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "REGEX",
|
||||
"regex": "[\\s\\S]*\\[\\{\"type\":\"text\",\"text\":\"You are an expert n8n workflow builder\\. You generate com[\\s\\S]*\"type\"\\s*:\\s*\"tool_use\"[\\s\\S]{0,300}\"name\"\\s*:\\s*\"build-workflow\"[\\s\\S]*"
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"x-envoy-upstream-service-time": [
|
||||
"1027"
|
||||
],
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"set-cookie": [
|
||||
"_cfuvid=dR33gMXkzAc6j93mq2ZP3OP4FErgVaPt3SdJgr02y6Q-1777373966.9623053-1.0.1.1-53qNyPi9.nhalsyxtD_3E9Hd2o3Oa.lByByK6yec8yk; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
|
||||
],
|
||||
"server-timing": [
|
||||
"x-originResponse;dur=1031"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CaW3ywEz1up5JV3ACYHNi"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-04-28T10:59:27Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"26966000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"27000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-04-28T10:59:27Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19998"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-04-28T10:59:27Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-04-28T10:59:27Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"22466000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"22500000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Tue, 28 Apr 2026 10:59:28 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"9f358ebd8c03b58b-PRG"
|
||||
]
|
||||
},
|
||||
"cookies": {
|
||||
"_cfuvid": "dR33gMXkzAc6j93mq2ZP3OP4FErgVaPt3SdJgr02y6Q-1777373966.9623053-1.0.1.1-53qNyPi9.nhalsyxtD_3E9Hd2o3Oa.lByByK6yec8yk"
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_01TMAHdn4NsZzBxnR9hqa9JW\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":695,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":21523,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":3,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Workflow is\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" ready. Click the Manual Trigger and hit \\\"Test workflow\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"\\\" to run it — then customize the Set node to add whatever fields you need.\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":695,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":21523,\"output_tokens\":35} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxVE1BSGRuNE5zWnpCeG5SOWhxYTlKVyIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjY5NSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MjE1MjMsImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5zIjozLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCIsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwifX0gICB9CgpldmVudDogY29udGVudF9ibG9ja19zdGFydApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0YXJ0IiwiaW5kZXgiOjAsImNvbnRlbnRfYmxvY2siOnsidHlwZSI6InRleHQiLCJ0ZXh0IjoiIn0gICAgICAgfQoKZXZlbnQ6IHBpbmcKZGF0YTogeyJ0eXBlIjogInBpbmcifQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiJXb3JrZmxvdyBpcyJ9fQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgcmVhZHkuIENsaWNrIHRoZSBNYW51YWwgVHJpZ2dlciBhbmQgaGl0IFwiVGVzdCB3b3JrZmxvdyJ9ICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiJcIiB0byBydW4gaXQg4oCUIHRoZW4gY3VzdG9taXplIHRoZSBTZXQgbm9kZSB0byBhZGQgd2hhdGV2ZXIgZmllbGRzIHlvdSBuZWVkLiJ9ICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjAgICAgICAgICAgICAgICB9CgpldmVudDogbWVzc2FnZV9kZWx0YQpkYXRhOiB7InR5cGUiOiJtZXNzYWdlX2RlbHRhIiwiZGVsdGEiOnsic3RvcF9yZWFzb24iOiJlbmRfdHVybiIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInN0b3BfZGV0YWlscyI6bnVsbH0sInVzYWdlIjp7ImlucHV0X3Rva2VucyI6Njk1LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjoyMTUyMywib3V0cHV0X3Rva2VucyI6MzV9ICAgfQoKZXZlbnQ6IG1lc3NhZ2Vfc3RvcApkYXRhOiB7InR5cGUiOiJtZXNzYWdlX3N0b3AiICAgICAgICAgfQoK",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "1777373979523-unknown-host-POST-_v1_messages-e7a67275.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "REGEX",
|
||||
"regex": "[\\s\\S]*\\[\\{\"type\":\"text\",\"text\":\"You are the n8n Instance Agent — an AI assistant embedde[\\s\\S]*Set up the workflow named \\\\\"B3 Full Wizard Apply\\\\\"\\.[\\s\\S]*"
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"x-envoy-upstream-service-time": [
|
||||
"1129"
|
||||
],
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"set-cookie": [
|
||||
"_cfuvid=pnbMU0JKL6VIeGtoQmnMJjyYTJBjJYTB19126AmlRvI-1777373973.2591949-1.0.1.1-JHRmYS_Sh_ZJas5yscrNAHLciVGqvmac3_N.GZu4DgI; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
|
||||
],
|
||||
"server-timing": [
|
||||
"x-originResponse;dur=1131"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CaW3zQAEertU27yw7WpZR"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-04-28T10:59:33Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"26975000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"27000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-04-28T10:59:33Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19998"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-04-28T10:59:33Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-04-28T10:59:33Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"22475000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"22500000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Tue, 28 Apr 2026 10:59:34 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"9f358ee4d86ddf72-PRG"
|
||||
]
|
||||
},
|
||||
"cookies": {
|
||||
"_cfuvid": "pnbMU0JKL6VIeGtoQmnMJjyYTJBjJYTB19126AmlRvI-1777373973.2591949-1.0.1.1-JHRmYS_Sh_ZJas5yscrNAHLciVGqvmac3_N.GZu4DgI"
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_01KTnakuKpWLGnRMwaQzgKdV\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":337,\"cache_creation_input_tokens\":14091,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":14091,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Let\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" me look up that workflow first.\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_01A94TsW82xTinxnAX4yAVF6\",\"name\":\"workflows\",\"input\":{},\"caller\":{\"type\":\"direct\"}} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"act\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"io\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"n\\\": \"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"list\\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\", \\\"query\\\": \"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"B3 \"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"Full Wiza\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"rd Apply\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"}\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":337,\"cache_creation_input_tokens\":14091,\"cache_read_input_tokens\":0,\"output_tokens\":83} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxS1RuYWt1S3BXTEduUk13YVF6Z0tkViIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjMzNywiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjoxNDA5MSwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjE0MDkxLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6MSwic2VydmljZV90aWVyIjoic3RhbmRhcmQiLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIn19ICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdGFydApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0YXJ0IiwiaW5kZXgiOjAsImNvbnRlbnRfYmxvY2siOnsidHlwZSI6InRleHQiLCJ0ZXh0IjoiIn0gICAgICAgfQoKZXZlbnQ6IHBpbmcKZGF0YTogeyJ0eXBlIjogInBpbmcifQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiJMZXQifX0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiIG1lIGxvb2sgdXAgdGhhdCB3b3JrZmxvdyBmaXJzdC4ifX0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfc3RhcnQKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdGFydCIsImluZGV4IjoxLCJjb250ZW50X2Jsb2NrIjp7InR5cGUiOiJ0b29sX3VzZSIsImlkIjoidG9vbHVfMDFBOTRUc1c4MnhUaW54bkFYNHlBVkY2IiwibmFtZSI6IndvcmtmbG93cyIsImlucHV0Ijp7fSwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifX0gICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IiJ9fQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IntcImFjdCJ9ICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJpbyJ9ICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJuXCI6ICJ9ICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiXCJsaXN0XCIifSAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiIsIFwicXVlcnlcIjogIn0gIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJcIkIzICJ9IH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJGdWxsIFdpemEifSAgICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6InJkIEFwcGx5In0gICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiXCJ9In0gICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdG9wCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RvcCIsImluZGV4IjoxICAgICAgICAgICB9CgpldmVudDogbWVzc2FnZV9kZWx0YQpkYXRhOiB7InR5cGUiOiJtZXNzYWdlX2RlbHRhIiwiZGVsdGEiOnsic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInN0b3BfZGV0YWlscyI6bnVsbH0sInVzYWdlIjp7ImlucHV0X3Rva2VucyI6MzM3LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjE0MDkxLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwib3V0cHV0X3Rva2VucyI6ODN9ICAgIH0KCmV2ZW50OiBtZXNzYWdlX3N0b3AKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdG9wIiAgICAgIH0KCg==",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "1777373979524-unknown-host-POST-_v1_messages-abc49c4f.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "REGEX",
|
||||
"regex": "[\\s\\S]*\\[\\{\"type\":\"text\",\"text\":\"You are the n8n Instance Agent — an AI assistant embedde[\\s\\S]*\"type\"\\s*:\\s*\"tool_use\"[\\s\\S]{0,300}\"name\"\\s*:\\s*\"workflows\"[\\s\\S]{0,500}\"action\"\\s*:\\s*\"list\"[\\s\\S]*"
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"x-envoy-upstream-service-time": [
|
||||
"1417"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"set-cookie": [
|
||||
"_cfuvid=7zOcfTVfjLjlOdvU7FDFjOiBGngj.K.vU1IjWO.Cf3U-1777373975.836431-1.0.1.1-VJw6NTGZjJMQCc02tAtKZbsaW0ISTztfRQAjr6vOBEg; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CaW3zbFjE4WwUkSNComLY"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-04-28T10:59:35Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"26975000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"27000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-04-28T10:59:35Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19998"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-04-28T10:59:35Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-04-28T10:59:36Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"22475000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"22500000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Tue, 28 Apr 2026 10:59:37 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"9f358ef4fb3cbb9c-PRG"
|
||||
]
|
||||
},
|
||||
"cookies": {
|
||||
"_cfuvid": "7zOcfTVfjLjlOdvU7FDFjOiBGngj.K.vU1IjWO.Cf3U-1777373975.836431-1.0.1.1-VJw6NTGZjJMQCc02tAtKZbsaW0ISTztfRQAjr6vOBEg"
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_018sM6MviEiG3T1QP1ktkjqj\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":541,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":14091,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":68,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_01Qm84eUUf1P3CfD4Yk56bSc\",\"name\":\"workflows\",\"input\":{},\"caller\":{\"type\":\"direct\"}} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"act\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"ion\\\": \\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"setup\\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\", \\\"work\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"flow\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"Id\\\": \\\"cKA\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"KMg8cquA\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"9y35\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"K\\\"}\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":541,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":14091,\"output_tokens\":84} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxOHNNNk12aUVpRzNUMVFQMWt0a2pxaiIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjU0MSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MTQwOTEsImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5zIjo2OCwic2VydmljZV90aWVyIjoic3RhbmRhcmQiLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIn19ICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfc3RhcnQKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdGFydCIsImluZGV4IjowLCJjb250ZW50X2Jsb2NrIjp7InR5cGUiOiJ0b29sX3VzZSIsImlkIjoidG9vbHVfMDFRbTg0ZVVVZjFQM0NmRDRZazU2YlNjIiwibmFtZSI6IndvcmtmbG93cyIsImlucHV0Ijp7fSwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifX0gICAgICAgICAgICAgfQoKZXZlbnQ6IHBpbmcKZGF0YTogeyJ0eXBlIjogInBpbmcifQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IiJ9IH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJ7XCIifSAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiYWN0In0gICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiaW9uXCI6IFwiIn0gICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6InNldHVwXCIifSAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IiwgXCJ3b3JrIn0gICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJmbG93In0gICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJJZFwiOiBcImNLQSJ9IH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJLTWc4Y3F1QSJ9ICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiI5eTM1In0gICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiS1wifSJ9ICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdG9wCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RvcCIsImluZGV4IjowICAgICAgICAgICAgfQoKZXZlbnQ6IG1lc3NhZ2VfZGVsdGEKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9kZWx0YSIsImRlbHRhIjp7InN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJzdG9wX2RldGFpbHMiOm51bGx9LCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjU0MSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MTQwOTEsIm91dHB1dF90b2tlbnMiOjg0fSAgICAgICB9CgpldmVudDogbWVzc2FnZV9zdG9wCmRhdGE6IHsidHlwZSI6Im1lc3NhZ2Vfc3RvcCJ9Cgo=",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "1777373979525-unknown-host-POST-_v1_messages-c17e02f9.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
{"kind":"header","version":1,"testName":"recording","recordedAt":"2026-04-28T10:59:33.145Z"}
|
||||
{"kind":"tool-call","stepId":1,"agentRole":"orchestrator","toolName":"workflows","input":{"action":"list","query":"B3 Full Wizard Apply"},"output":{"workflows":[{"id":"cKAKMg8cquA9y35K","name":"B3 Full Wizard Apply","versionId":"1e867e5e-50fd-4326-8c0f-6ad69d9616aa","activeVersionId":null,"createdAt":"2026-04-28T10:59:29.306Z","updatedAt":"2026-04-28T10:59:29.306Z"}]}}
|
||||
{"kind":"tool-call","stepId":2,"agentRole":"orchestrator","toolName":"workflows","input":{"action":"setup","workflowId":"cKAKMg8cquA9y35K"},"output":{"success":false}}
|
||||
{"kind":"tool-resume","stepId":3,"agentRole":"orchestrator","toolName":"workflows","input":{"action":"setup","workflowId":"cKAKMg8cquA9y35K"},"output":{"success":true,"completedNodes":[{"nodeName":"HTTP Request","parametersSet":["url"]}],"updatedNodes":[{"id":"trigger","name":"Manual Trigger","type":"n8n-nodes-base.manualTrigger","typeVersion":1,"position":[0,0],"parameters":{}},{"id":"http","name":"HTTP Request","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[220,-120],"parameters":{"method":"GET","url":"https://example.com/api","authentication":"none"}},{"id":"slack","name":"Slack","type":"n8n-nodes-base.slack","typeVersion":2.3,"position":[220,120],"parameters":{"resource":"message","operation":"post","select":"channel","channelId":{"__rl":true,"value":"C0000000000","mode":"id"},"text":"hello from setup e2e"},"credentials":{"slackApi":{"id":"oR4HYz4azgE1NUGj","name":"B3 Apply Slack Primary"}}}],"updatedConnections":{"Manual Trigger":{"main":[[{"node":"HTTP Request","type":"main","index":0},{"node":"Slack","type":"main","index":0}]]}}},"resumeData":{"approved":true,"credentials":{},"action":"apply","nodeParameters":{"HTTP Request":{"url":"https://example.com/api"}}}}
|
||||
{"kind":"header","version":1,"testName":"recording","recordedAt":"2026-05-05T09:48:51.817Z"}
|
||||
{"kind":"tool-call","stepId":1,"agentRole":"orchestrator","toolName":"workflows","input":{"action":"list","query":"B3 Full Wizard Apply"},"output":{"workflows":[{"id":"TqaMpj2sVOinXnc2","name":"B3 Full Wizard Apply","versionId":"ba829dcd-395c-4fe4-8f23-43f8ca963363","activeVersionId":null,"createdAt":"2026-05-05T09:48:47.635Z","updatedAt":"2026-05-05T09:48:47.635Z"}]}}
|
||||
{"kind":"tool-call","stepId":2,"agentRole":"orchestrator","toolName":"workflows","input":{"action":"setup","workflowId":"TqaMpj2sVOinXnc2"},"output":{"success":false}}
|
||||
{"kind":"tool-resume","stepId":3,"agentRole":"orchestrator","toolName":"workflows","input":{"action":"setup","workflowId":"TqaMpj2sVOinXnc2"},"output":{"success":true,"completedNodes":[{"nodeName":"HTTP Request","credentialType":"httpHeaderAuth","parametersSet":["url"]}],"updatedNodes":[{"id":"trigger","name":"Manual Trigger","type":"n8n-nodes-base.manualTrigger","typeVersion":1,"position":[0,0],"parameters":{}},{"id":"http","name":"HTTP Request","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[220,0],"parameters":{"method":"GET","url":"https://example.com/api","authentication":"genericCredentialType","genericAuthType":"httpHeaderAuth"},"credentials":{"httpHeaderAuth":{"id":"00vl3Jz8mVoLa7Vx","name":"B3 Parameter Header Auth"}}}],"updatedConnections":{"Manual Trigger":{"main":[[{"node":"HTTP Request","type":"main","index":0}]]}}},"resumeData":{"approved":true,"credentials":{"HTTP Request":{"httpHeaderAuth":"00vl3Jz8mVoLa7Vx"}},"action":"apply","nodeParameters":{"HTTP Request":{"url":"https://example.com/api"}}}}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "REGEX",
|
||||
"regex": "[\\s\\S]*\\[\\{\"type\":\"text\",\"text\":\"You are the n8n Instance Agent — an AI assistant embedde[\\s\\S]*Set up the workflow named \\\\\"B3 Workflow Setup Required Parameter\\\\\"\\.[\\s\\S]*"
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"x-envoy-upstream-service-time": [
|
||||
"1097"
|
||||
],
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"traceresponse": [
|
||||
"00-055e95bb61ad3a581def7cc481d142c3-c5c8f23e44a58a6d-01"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"set-cookie": [
|
||||
"_cfuvid=25c5Yn3ZqlCuityQInH8XiuYNdLtqIj7W8cmunRCqT4-1777974520.6361456-1.0.1.1-SB7A0ms2uQtXbGXDPhfow7KZVLZ2tnn0Fdx.lvfG00w; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
|
||||
],
|
||||
"server-timing": [
|
||||
"x-originResponse;dur=1106"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CajDFLCNVz755J5ZKu2hP"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-05-05T09:48:40Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"26975000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"27000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-05-05T09:48:40Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19998"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-05-05T09:48:40Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-05-05T09:48:40Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"22475000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"22500000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Tue, 05 May 2026 09:48:41 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"9f6ed4b1fd9f3402-SOF"
|
||||
]
|
||||
},
|
||||
"cookies": {
|
||||
"_cfuvid": "25c5Yn3ZqlCuityQInH8XiuYNdLtqIj7W8cmunRCqT4-1777974520.6361456-1.0.1.1-SB7A0ms2uQtXbGXDPhfow7KZVLZ2tnn0Fdx.lvfG00w"
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_013EmqGt1UJyrCs1kdNCcZ1B\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":338,\"cache_creation_input_tokens\":13705,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":13705,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"I\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"'ll look up that workflow for you right away.\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_01Anjf1vTeRd67d4z6cvPFdX\",\"name\":\"workflows\",\"input\":{},\"caller\":{\"type\":\"direct\"}} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"action\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\": \\\"list\\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\", \\\"query\\\":\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\" \\\"B3\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\" Workflo\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"w Setup Re\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"qu\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"ired\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\" Param\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"eter\\\"}\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":338,\"cache_creation_input_tokens\":13705,\"cache_read_input_tokens\":0,\"output_tokens\":87} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxM0VtcUd0MVVKeXJDczFrZE5DY1oxQiIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjMzOCwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjoxMzcwNSwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjEzNzA1LCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6MSwic2VydmljZV90aWVyIjoic3RhbmRhcmQiLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIn19ICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdGFydApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0YXJ0IiwiaW5kZXgiOjAsImNvbnRlbnRfYmxvY2siOnsidHlwZSI6InRleHQiLCJ0ZXh0IjoiIn0gICAgICAgICAgICAgfQoKZXZlbnQ6IHBpbmcKZGF0YTogeyJ0eXBlIjogInBpbmcifQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiJJIn0gICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IidsbCBsb29rIHVwIHRoYXQgd29ya2Zsb3cgZm9yIHlvdSByaWdodCBhd2F5LiJ9IH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0YXJ0CmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RhcnQiLCJpbmRleCI6MSwiY29udGVudF9ibG9jayI6eyJ0eXBlIjoidG9vbF91c2UiLCJpZCI6InRvb2x1XzAxQW5qZjF2VGVSZDY3ZDR6NmN2UEZkWCIsIm5hbWUiOiJ3b3JrZmxvd3MiLCJpbnB1dCI6e30sImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In19ICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiIifSAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IntcImFjdGlvbiJ9IH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJcIjogXCJsaXN0XCIifSAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiLCBcInF1ZXJ5XCI6In19CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiIFwiQjMifSAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IiBXb3JrZmxvIn0gICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJ3IFNldHVwIFJlIn0gICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6InF1In0gfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6ImlyZWQifSAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiIgUGFyYW0ifSAgICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6ImV0ZXJcIn0ifSAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjEgICAgICAgICAgIH0KCmV2ZW50OiBtZXNzYWdlX2RlbHRhCmRhdGE6IHsidHlwZSI6Im1lc3NhZ2VfZGVsdGEiLCJkZWx0YSI6eyJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsfSwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjozMzgsImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MTM3MDUsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJvdXRwdXRfdG9rZW5zIjo4N30gICAgICAgICAgIH0KCmV2ZW50OiBtZXNzYWdlX3N0b3AKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdG9wIiAgICAgICAgICAgIH0KCg==",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "0000-1777974525416-unknown-host-POST-_v1_messages-60adeed8.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "REGEX",
|
||||
"regex": "[\\s\\S]*\\[\\{\"type\":\"text\",\"text\":\"You are the n8n Instance Agent — an AI assistant embedde[\\s\\S]*\"type\"\\s*:\\s*\"tool_use\"[\\s\\S]{0,300}\"name\"\\s*:\\s*\"workflows\"[\\s\\S]{0,500}\"action\"\\s*:\\s*\"list\"[\\s\\S]*"
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"x-envoy-upstream-service-time": [
|
||||
"1465"
|
||||
],
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"traceresponse": [
|
||||
"00-c4bf6a54621c37b6ccb45ce230081995-7ad5ba507c1529cb-01"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"set-cookie": [
|
||||
"_cfuvid=ttfH162zobw9.tTADu7gmQVr0clci6NMsjoDRvAJ69g-1777974522.8463345-1.0.1.1-YIIhigPWxl_LGPv8xmi6PxCPYnP16T4ijo6LJwwWAJQ; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
|
||||
],
|
||||
"server-timing": [
|
||||
"x-originResponse;dur=1477"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CajDFVbvKiNGWLhrALLnn"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-05-05T09:48:42Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"26975000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"27000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-05-05T09:48:42Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19998"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-05-05T09:48:42Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-05-05T09:48:43Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"22475000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"22500000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Tue, 05 May 2026 09:48:44 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"9f6ed4bfcf508693-SOF"
|
||||
]
|
||||
},
|
||||
"cookies": {
|
||||
"_cfuvid": "ttfH162zobw9.tTADu7gmQVr0clci6NMsjoDRvAJ69g-1777974522.8463345-1.0.1.1-YIIhigPWxl_LGPv8xmi6PxCPYnP16T4ijo6LJwwWAJQ"
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_01Eeute14cfYF47DGKWT5UGz\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":201,\"cache_creation_input_tokens\":349,\"cache_read_input_tokens\":13705,\"cache_creation\":{\"ephemeral_5m_input_tokens\":349,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":68,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_01VErWsfZtqVuBTh3XZ3LKDA\",\"name\":\"workflows\",\"input\":{},\"caller\":{\"type\":\"direct\"}} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"action\\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\": \\\"setup\\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\", \\\"workflow\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"Id\\\": \\\"3jbqn6\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"Rx4TScS58\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"P\\\"}\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":201,\"cache_creation_input_tokens\":349,\"cache_read_input_tokens\":13705,\"output_tokens\":84} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxRWV1dGUxNGNmWUY0N0RHS1dUNVVHeiIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjIwMSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjozNDksImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjoxMzcwNSwiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MzQ5LCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6NjgsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIiwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCJ9fSAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0YXJ0CmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RhcnQiLCJpbmRleCI6MCwiY29udGVudF9ibG9jayI6eyJ0eXBlIjoidG9vbF91c2UiLCJpZCI6InRvb2x1XzAxVkVyV3NmWnRxVnVCVGgzWFozTEtEQSIsIm5hbWUiOiJ3b3JrZmxvd3MiLCJpbnB1dCI6e30sImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In19ICAgICAgIH0KCmV2ZW50OiBwaW5nCmRhdGE6IHsidHlwZSI6ICJwaW5nIn0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiIifSAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoie1wiYWN0aW9uXCIifSAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IjogXCJzZXR1cFwiIn0gICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiLCBcIndvcmtmbG93In0gICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IklkXCI6IFwiM2picW42In0gICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IlJ4NFRTY1M1OCJ9ICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJQXCJ9In0gICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjAgICAgICAgICAgICB9CgpldmVudDogbWVzc2FnZV9kZWx0YQpkYXRhOiB7InR5cGUiOiJtZXNzYWdlX2RlbHRhIiwiZGVsdGEiOnsic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInN0b3BfZGV0YWlscyI6bnVsbH0sInVzYWdlIjp7ImlucHV0X3Rva2VucyI6MjAxLCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjM0OSwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjEzNzA1LCJvdXRwdXRfdG9rZW5zIjo4NH0gICAgICAgIH0KCmV2ZW50OiBtZXNzYWdlX3N0b3AKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdG9wIiAgICAgICAgIH0KCg==",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "0001-1777974525417-unknown-host-POST-_v1_messages-c17e02f9.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,112 +0,0 @@
|
|||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "[{\"type\":\"text\",\"text\":\"You are the n8n Instance Agent — an AI assistant embedde",
|
||||
"subString": true
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"x-envoy-upstream-service-time": [
|
||||
"1080"
|
||||
],
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"set-cookie": [
|
||||
"_cfuvid=bHDKEwSZj0UbZxAc3kzMgNWY36YeQ_Wkck4I_6ElBE0-1776687502.979579-1.0.1.1-HpcePCCZ_s.G1MNIw7PTRA2yHfehuuwEuXx1Xd9o.9w; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
|
||||
],
|
||||
"server-timing": [
|
||||
"x-originResponse;dur=1082"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CaF1XisaG1B6X4RNVqcHC"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-04-20T12:18:23Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"26981000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"27000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-04-20T12:18:23Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19998"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-04-20T12:18:23Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-04-20T12:18:23Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"22481000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"22500000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Mon, 20 Apr 2026 12:18:24 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"9ef4175d980bdc1a-SOF"
|
||||
]
|
||||
},
|
||||
"cookies": {
|
||||
"_cfuvid": "bHDKEwSZj0UbZxAc3kzMgNWY36YeQ_Wkck4I_6ElBE0-1776687502.979579-1.0.1.1-HpcePCCZ_s.G1MNIw7PTRA2yHfehuuwEuXx1Xd9o.9w"
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_01C2H8f7mG5vTLLQhGQsBNBb\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":892,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":10216,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"The\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" build finished. Let me check the result and open the setup UI.\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_018UpsQynyaMDwpXbo1TLB5g\",\"name\":\"workflows\",\"input\":{},\"caller\":{\"type\":\"direct\"}} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"action\\\": \"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"list\\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\", \\\"qu\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"ery\\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\": \\\"Manu\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"al Tri\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"gger HTTP Re\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"quest\\\"}\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":892,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":10216,\"output_tokens\":88} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxQzJIOGY3bUc1dlRMTFFoR1FzQk5CYiIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjg5MiwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MTAyMTYsImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5zIjoxLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCIsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwifX0gICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfc3RhcnQKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdGFydCIsImluZGV4IjowLCJjb250ZW50X2Jsb2NrIjp7InR5cGUiOiJ0ZXh0IiwidGV4dCI6IiJ9ICAgfQoKZXZlbnQ6IHBpbmcKZGF0YTogeyJ0eXBlIjogInBpbmcifQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiJUaGUifSAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiBidWlsZCBmaW5pc2hlZC4gTGV0IG1lIGNoZWNrIHRoZSByZXN1bHQgYW5kIG9wZW4gdGhlIHNldHVwIFVJLiJ9ICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfc3RvcApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0b3AiLCJpbmRleCI6MCAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0YXJ0CmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RhcnQiLCJpbmRleCI6MSwiY29udGVudF9ibG9jayI6eyJ0eXBlIjoidG9vbF91c2UiLCJpZCI6InRvb2x1XzAxOFVwc1F5bnlhTUR3cFhibzFUTEI1ZyIsIm5hbWUiOiJ3b3JrZmxvd3MiLCJpbnB1dCI6e30sImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In19ICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiIn0gICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoie1wiIn0gICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJhY3Rpb25cIjogIn0gICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJcImxpc3RcIiJ9ICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IiwgXCJxdSJ9ICAgICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6ImVyeVwiIn0gfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IjogXCJNYW51In0gICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiYWwgVHJpIn19CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiZ2dlciBIVFRQIFJlIn0gICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6InF1ZXN0XCJ9In0gIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjEgICAgICAgICAgfQoKZXZlbnQ6IG1lc3NhZ2VfZGVsdGEKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9kZWx0YSIsImRlbHRhIjp7InN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJzdG9wX2RldGFpbHMiOm51bGx9LCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjg5MiwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MTAyMTYsIm91dHB1dF90b2tlbnMiOjg4fSAgIH0KCmV2ZW50OiBtZXNzYWdlX3N0b3AKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdG9wIiAgfQoK",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "1776687509148-unknown-host-POST-_v1_messages-8a23f6c2.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "[{\"type\":\"text\",\"text\":\"You are an expert n8n workflow builder. You generate com",
|
||||
"subString": true
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"x-envoy-upstream-service-time": [
|
||||
"1840"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"set-cookie": [
|
||||
"_cfuvid=B3YlQaTOUCq5YX.6Q.6NOvaLP44CbxrEhBLuzoU0H0E-1776687504.8906143-1.0.1.1-tL12YVyB0aycHSm2yFU11Sbbgt.YrN6jEC.9NHgqs78; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
|
||||
],
|
||||
"server-timing": [
|
||||
"x-originResponse;dur=1844"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CaF1Xs7BL1UZrtDShLWKY"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-04-20T12:18:25Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"26964000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"27000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-04-20T12:18:25Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19998"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-04-20T12:18:25Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-04-20T12:18:25Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"22464000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"22500000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Mon, 20 Apr 2026 12:18:26 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"9ef417698faadc3a-SOF"
|
||||
]
|
||||
},
|
||||
"cookies": {
|
||||
"_cfuvid": "B3YlQaTOUCq5YX.6Q.6NOvaLP44CbxrEhBLuzoU0H0E-1776687504.8906143-1.0.1.1-tL12YVyB0aycHSm2yFU11Sbbgt.YrN6jEC.9NHgqs78"
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_01AxZQvjdQ5bp7n8XudaqJTu\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":723,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":23018,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Done\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\". The workflow is built with a Manual Trigger connected to an HTTP Request node. The URL field is left blank\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" for you to fill in. You can open the HTTP Request node and enter your URL whenever\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" you're ready.\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":723,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":23018,\"output_tokens\":49} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxQXhaUXZqZFE1YnA3bjhYdWRhcUpUdSIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjcyMywiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MjMwMTgsImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5zIjoxLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCIsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwifX0gICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfc3RhcnQKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdGFydCIsImluZGV4IjowLCJjb250ZW50X2Jsb2NrIjp7InR5cGUiOiJ0ZXh0IiwidGV4dCI6IiJ9ICAgfQoKZXZlbnQ6IHBpbmcKZGF0YTogeyJ0eXBlIjogInBpbmcifQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiJEb25lIn0gICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIuIFRoZSB3b3JrZmxvdyBpcyBidWlsdCB3aXRoIGEgTWFudWFsIFRyaWdnZXIgY29ubmVjdGVkIHRvIGFuIEhUVFAgUmVxdWVzdCBub2RlLiBUaGUgVVJMIGZpZWxkIGlzIGxlZnQgYmxhbmsifSAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgZm9yIHlvdSB0byBmaWxsIGluLiBZb3UgY2FuIG9wZW4gdGhlIEhUVFAgUmVxdWVzdCBub2RlIGFuZCBlbnRlciB5b3VyIFVSTCB3aGVuZXZlciJ9ICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiB5b3UncmUgcmVhZHkuIn0gICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjAgICAgICAgIH0KCmV2ZW50OiBtZXNzYWdlX2RlbHRhCmRhdGE6IHsidHlwZSI6Im1lc3NhZ2VfZGVsdGEiLCJkZWx0YSI6eyJzdG9wX3JlYXNvbiI6ImVuZF90dXJuIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsfSwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjo3MjMsImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjIzMDE4LCJvdXRwdXRfdG9rZW5zIjo0OX0gICAgICAgIH0KCmV2ZW50OiBtZXNzYWdlX3N0b3AKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdG9wIiAgICB9Cgo=",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "1776687509149-unknown-host-POST-_v1_messages-4d1c93f7.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "[{\"type\":\"text\",\"text\":\"You are the n8n Instance Agent — an AI assistant embedde",
|
||||
"subString": true
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"x-envoy-upstream-service-time": [
|
||||
"1499"
|
||||
],
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"set-cookie": [
|
||||
"_cfuvid=nFEe9FD2a3EYbnGXy3muJuoPJomRyAN9n_YU8BntQCk-1776687505.58011-1.0.1.1-vMaUc8nGBB7TrIV0zjzo6tvmL214gtdAWNqRUkY41xs; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
|
||||
],
|
||||
"server-timing": [
|
||||
"x-originResponse;dur=1501"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CaF1Xv1Jm8rBhNF3rYNfz"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-04-20T12:18:25Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"26981000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"27000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-04-20T12:18:25Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19998"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-04-20T12:18:25Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-04-20T12:18:25Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"22481000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"22500000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Mon, 20 Apr 2026 12:18:27 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"9ef4176dd8013130-SOF"
|
||||
]
|
||||
},
|
||||
"cookies": {
|
||||
"_cfuvid": "nFEe9FD2a3EYbnGXy3muJuoPJomRyAN9n_YU8BntQCk-1776687505.58011-1.0.1.1-vMaUc8nGBB7TrIV0zjzo6tvmL214gtdAWNqRUkY41xs"
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_014fQPmj4epDVaCfp2Ny5gyf\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":1101,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":10216,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":57,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_01GKcDBgzprj2kT4zpjV9W3M\",\"name\":\"workflows\",\"input\":{},\"caller\":{\"type\":\"direct\"}} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"act\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"ion\\\": \\\"se\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"tup\\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\", \\\"wor\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"kflo\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"wI\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"d\\\": \\\"vZSmhQk\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"Ge5S\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"p1s\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"Op\\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\", \\\"work\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"flowNa\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"me\\\":\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\" \\\"Manual Tri\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"gger - HTT\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"P Request\\\"}\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":1101,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":10216,\"output_tokens\":110} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxNGZRUG1qNGVwRFZhQ2ZwMk55NWd5ZiIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjExMDEsImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjEwMjE2LCJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6NTcsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIiwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCJ9fSAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfc3RhcnQKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdGFydCIsImluZGV4IjowLCJjb250ZW50X2Jsb2NrIjp7InR5cGUiOiJ0b29sX3VzZSIsImlkIjoidG9vbHVfMDFHS2NEQmd6cHJqMmtUNHpwalY5VzNNIiwibmFtZSI6IndvcmtmbG93cyIsImlucHV0Ijp7fSwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifX0gICAgICAgICAgIH0KCmV2ZW50OiBwaW5nCmRhdGE6IHsidHlwZSI6ICJwaW5nIn0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiIifSAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJ7XCJhY3QifSAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiaW9uXCI6IFwic2UifSAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6InR1cFwiIn0gICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IiwgXCJ3b3IifSAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoia2ZsbyJ9ICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IndJIn0gICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiZFwiOiBcInZaU21oUWsifSAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiR2U1UyJ9ICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJwMXMifSAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJPcFwiIn0gfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IiwgXCJ3b3JrIn0gICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJmbG93TmEifSAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoibWVcIjoifSAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiIFwiTWFudWFsIFRyaSJ9ICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6ImdnZXIgLSBIVFQifSAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IlAgUmVxdWVzdFwifSJ9ICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjAgICAgICAgICB9CgpldmVudDogbWVzc2FnZV9kZWx0YQpkYXRhOiB7InR5cGUiOiJtZXNzYWdlX2RlbHRhIiwiZGVsdGEiOnsic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInN0b3BfZGV0YWlscyI6bnVsbH0sInVzYWdlIjp7ImlucHV0X3Rva2VucyI6MTEwMSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MTAyMTYsIm91dHB1dF90b2tlbnMiOjExMH0gICAgICB9CgpldmVudDogbWVzc2FnZV9zdG9wCmRhdGE6IHsidHlwZSI6Im1lc3NhZ2Vfc3RvcCIgICAgICAgICAgICB9Cgo=",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "1776687509149-unknown-host-POST-_v1_messages-8a23f6c2.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,3 @@
|
|||
{"kind":"header","version":1,"testName":"recording","recordedAt":"2026-04-20T12:18:08.934Z"}
|
||||
{"kind":"tool-call","stepId":1,"agentRole":"orchestrator","toolName":"build-workflow-with-agent","input":{"task":"Build a workflow with a Manual Trigger node connected to an HTTP Request node. Leave the URL field on the HTTP Request node empty/blank — do not fill in any URL. The user will configure it themselves after the workflow is built.","conversationContext":"User wants a simple workflow: Manual Trigger → HTTP Request node, with the URL left empty for them to fill in later. After building, the setup UI should be opened."},"output":{"result":"Workflow build started (task: build-JQ794Gxg). Reply with one short sentence — e.g. name what's being built. Do NOT summarize the plan or list details.","taskId":"build-JQ794Gxg"}}
|
||||
{"kind":"tool-call","stepId":2,"agentRole":"orchestrator","toolName":"task-control","input":{"action":"update-checklist","tasks":[{"id":"1","description":"Build workflow with Manual Trigger and HTTP Request node (URL left empty)","status":"in_progress"},{"id":"2","description":"Open setup UI for the workflow","status":"todo"}]},"output":{"saved":true}}
|
||||
{"kind":"tool-call","stepId":3,"agentRole":"orchestrator","toolName":"task-control","input":{"action":"update-checklist","tasks":[{"id":"1","description":"Build workflow with Manual Trigger and HTTP Request node (URL left empty)","status":"done"},{"id":"2","description":"Open setup UI for the workflow","status":"in_progress"}]},"output":{"saved":true}}
|
||||
{"kind":"tool-call","stepId":4,"agentRole":"workflow-builder","toolName":"build-workflow","input":{"code":"\nimport { workflow, node, trigger } from '@n8n/workflow-sdk';\n\nconst manualTrigger = trigger({\n type: 'n8n-nodes-base.manualTrigger',\n version: 1,\n config: { name: 'Manual Trigger' }\n});\n\nconst httpRequest = node({\n type: 'n8n-nodes-base.httpRequest',\n version: 4.3,\n config: {\n name: 'HTTP Request',\n parameters: {\n method: 'GET',\n url: ''\n }\n }\n});\n\nexport default workflow('manual-http', 'Manual Trigger - HTTP Request')\n .add(manualTrigger)\n .to(httpRequest);\n","name":"Manual Trigger - HTTP Request"},"output":{"success":true,"workflowId":"vZSmhQkGe5Sp1sOp"}}
|
||||
{"kind":"tool-call","stepId":5,"agentRole":"orchestrator","toolName":"workflows","input":{"action":"list","query":"Manual Trigger HTTP Request"},"output":{"workflows":[{"id":"vZSmhQkGe5Sp1sOp","name":"Manual Trigger - HTTP Request","versionId":"fe5da20e-be6e-47f5-bf23-6baa86c174a0","activeVersionId":null,"createdAt":"2026-04-20T12:18:23.979Z","updatedAt":"2026-04-20T12:18:24.662Z"}]}}
|
||||
{"kind":"tool-call","stepId":6,"agentRole":"orchestrator","toolName":"workflows","input":{"action":"setup","workflowId":"vZSmhQkGe5Sp1sOp","workflowName":"Manual Trigger - HTTP Request"},"output":{"success":false}}
|
||||
{"kind":"header","version":1,"testName":"recording","recordedAt":"2026-05-05T09:48:40.369Z"}
|
||||
{"kind":"tool-call","stepId":1,"agentRole":"orchestrator","toolName":"workflows","input":{"action":"list","query":"B3 Workflow Setup Required Parameter"},"output":{"workflows":[{"id":"3jbqn6Rx4TScS58P","name":"B3 Workflow Setup Required Parameter","versionId":"cb6f9a5e-1491-4d19-a1f9-98663eeea4f2","activeVersionId":null,"createdAt":"2026-05-05T09:48:36.418Z","updatedAt":"2026-05-05T09:48:36.418Z"}]}}
|
||||
{"kind":"tool-call","stepId":2,"agentRole":"orchestrator","toolName":"workflows","input":{"action":"setup","workflowId":"3jbqn6Rx4TScS58P"},"output":{"success":false}}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "REGEX",
|
||||
"regex": "[\\s\\S]*\\[\\{\"type\":\"text\",\"text\":\"You are the n8n Instance Agent — an AI assistant embedde[\\s\\S]*Set up the workflow named \\\\\"B3 Workflow Setup Slack Credential Test\\\\\"\\.[\\s\\S]*"
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"x-envoy-upstream-service-time": [
|
||||
"724"
|
||||
],
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"traceresponse": [
|
||||
"00-557c876fd60eb3af3ac698bff7cda58d-197afb4db1546736-01"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"set-cookie": [
|
||||
"_cfuvid=ExuHe4fibDRZVW53_CUUmseaqSsNQb.2BcmRbIFoR00-1777974547.672693-1.0.1.1-nd52Sh2u8G3QW.N35H2DUkleDZkmwrJ7jiYm6wVne4o; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
|
||||
],
|
||||
"server-timing": [
|
||||
"x-originResponse;dur=732"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CajDHKjuCc56JNr1mFWE5"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-05-05T09:49:07Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"26975000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"27000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-05-05T09:49:07Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19998"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-05-05T09:49:07Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-05-05T09:49:07Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"22475000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"22500000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Tue, 05 May 2026 09:49:08 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"9f6ed55af81c2ba5-SOF"
|
||||
]
|
||||
},
|
||||
"cookies": {
|
||||
"_cfuvid": "ExuHe4fibDRZVW53_CUUmseaqSsNQb.2BcmRbIFoR00-1777974547.672693-1.0.1.1-nd52Sh2u8G3QW.N35H2DUkleDZkmwrJ7jiYm6wVne4o"
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_01MXmaHCX5YrhS7captkqTza\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":341,\"cache_creation_input_tokens\":13705,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":13705,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Let\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" me find that workflow first.\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_01S2pJnjacMXRybkqtH8Dnko\",\"name\":\"workflows\",\"input\":{},\"caller\":{\"type\":\"direct\"}} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"action\\\": \"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"li\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"st\\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\", \\\"quer\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"y\\\": \\\"B3 Work\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"flow\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\" S\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"etup Slack\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\" C\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"redenti\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"al Te\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"st\\\"}\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":341,\"cache_creation_input_tokens\":13705,\"cache_read_input_tokens\":0,\"output_tokens\":86} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxTVhtYUhDWDVZcmhTN2NhcHRrcVR6YSIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjM0MSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjoxMzcwNSwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjEzNzA1LCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6MSwic2VydmljZV90aWVyIjoic3RhbmRhcmQiLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIn19ICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdGFydApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0YXJ0IiwiaW5kZXgiOjAsImNvbnRlbnRfYmxvY2siOnsidHlwZSI6InRleHQiLCJ0ZXh0IjoiIn0gICAgICAgICAgfQoKZXZlbnQ6IHBpbmcKZGF0YTogeyJ0eXBlIjogInBpbmcifQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiJMZXQifSAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiBtZSBmaW5kIHRoYXQgd29ya2Zsb3cgZmlyc3QuIn0gIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdGFydApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0YXJ0IiwiaW5kZXgiOjEsImNvbnRlbnRfYmxvY2siOnsidHlwZSI6InRvb2xfdXNlIiwiaWQiOiJ0b29sdV8wMVMycEpuamFjTVhSeWJrcXRIOERua28iLCJuYW1lIjoid29ya2Zsb3dzIiwiaW5wdXQiOnt9LCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9fSAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiIn19CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoie1wiYWN0aW9uXCI6ICJ9ICAgICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IlwibGkifSAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoic3RcIiJ9ICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiLCBcInF1ZXIifSAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoieVwiOiBcIkIzIFdvcmsifX0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJmbG93In19CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiIFMifSAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiZXR1cCBTbGFjayJ9ICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IiBDIn19CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoicmVkZW50aSJ9ICAgICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6ImFsIFRlIn0gICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJzdFwifSJ9ICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfc3RvcApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0b3AiLCJpbmRleCI6MSAgfQoKZXZlbnQ6IG1lc3NhZ2VfZGVsdGEKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9kZWx0YSIsImRlbHRhIjp7InN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJzdG9wX2RldGFpbHMiOm51bGx9LCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjM0MSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjoxMzcwNSwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsIm91dHB1dF90b2tlbnMiOjg2fSAgIH0KCmV2ZW50OiBtZXNzYWdlX3N0b3AKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdG9wIiAgICAgICB9Cgo=",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "0000-1777974556274-unknown-host-POST-_v1_messages-cd560ab0.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "REGEX",
|
||||
"regex": "[\\s\\S]*\\[\\{\"type\":\"text\",\"text\":\"You are the n8n Instance Agent — an AI assistant embedde[\\s\\S]*\"type\"\\s*:\\s*\"tool_use\"[\\s\\S]{0,300}\"name\"\\s*:\\s*\"workflows\"[\\s\\S]{0,500}\"action\"\\s*:\\s*\"list\"[\\s\\S]*"
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"x-envoy-upstream-service-time": [
|
||||
"1200"
|
||||
],
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"traceresponse": [
|
||||
"00-33057778e4db9a3cf76b3142c14a3dd1-b75041e007a2329b-01"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"set-cookie": [
|
||||
"_cfuvid=94hDettW.Ao0R7qDRThqdQFRFClOv6K8EClHAYDKakE-1777974549.6707172-1.0.1.1-hKAhZHmfSuFixJDKHxmivT9ZDeI9jfhazRMxpCMODBI; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CajDHUPoSVTnLuQX3mByj"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-05-05T09:49:09Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"26940000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"27000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-05-05T09:49:09Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19998"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-05-05T09:49:09Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-05-05T09:49:09Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"22440000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"22500000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Tue, 05 May 2026 09:49:11 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"9f6ed5676e803135-SOF"
|
||||
]
|
||||
},
|
||||
"cookies": {
|
||||
"_cfuvid": "94hDettW.Ao0R7qDRThqdQFRFClOv6K8EClHAYDKakE-1777974549.6707172-1.0.1.1-hKAhZHmfSuFixJDKHxmivT9ZDeI9jfhazRMxpCMODBI"
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_01SpZzridkSEzGXUrdF6wmfu\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":204,\"cache_creation_input_tokens\":14053,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":14053,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":58,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_01VAiV26VCFcz6GfUHbLTQuL\",\"name\":\"workflows\",\"input\":{},\"caller\":{\"type\":\"direct\"}} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"action\\\":\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\" \\\"setup\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\", \\\"wor\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"kflowId\\\": \\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"fF6E\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"AxQoY1E6TMjK\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"}\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":204,\"cache_creation_input_tokens\":14053,\"cache_read_input_tokens\":0,\"output_tokens\":84} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxU3BaenJpZGtTRXpHWFVyZEY2d21mdSIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjIwNCwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjoxNDA1MywiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjE0MDUzLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6NTgsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIiwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCJ9fSAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdGFydApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0YXJ0IiwiaW5kZXgiOjAsImNvbnRlbnRfYmxvY2siOnsidHlwZSI6InRvb2xfdXNlIiwiaWQiOiJ0b29sdV8wMVZBaVYyNlZDRmN6NkdmVUhiTFRRdUwiLCJuYW1lIjoid29ya2Zsb3dzIiwiaW5wdXQiOnt9LCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9fSAgICAgICAgICAgfQoKZXZlbnQ6IHBpbmcKZGF0YTogeyJ0eXBlIjogInBpbmcifQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IiJ9ICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoie1wiYWN0aW9uXCI6In0gICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiIgXCJzZXR1cCJ9ICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IlwiIn0gICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiLCBcIndvciJ9IH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJrZmxvd0lkXCI6IFwiIn0gICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiZkY2RSJ9fQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IkF4UW9ZMUU2VE1qSyJ9ICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiXCJ9In0gICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjAgICAgICAgICB9CgpldmVudDogbWVzc2FnZV9kZWx0YQpkYXRhOiB7InR5cGUiOiJtZXNzYWdlX2RlbHRhIiwiZGVsdGEiOnsic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInN0b3BfZGV0YWlscyI6bnVsbH0sInVzYWdlIjp7ImlucHV0X3Rva2VucyI6MjA0LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjE0MDUzLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwib3V0cHV0X3Rva2VucyI6ODR9ICAgICAgICAgICAgIH0KCmV2ZW50OiBtZXNzYWdlX3N0b3AKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdG9wIiAgICAgICAgICAgICAgfQoK",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "0001-1777974556275-unknown-host-POST-_v1_messages-c17e02f9.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "REGEX",
|
||||
"regex": "[\\s\\S]*\\[\\{\"type\":\"text\",\"text\":\"You are the n8n Instance Agent — an AI assistant embedde[\\s\\S]*\"type\"\\s*:\\s*\"tool_use\"[\\s\\S]{0,300}\"name\"\\s*:\\s*\"workflows\"[\\s\\S]{0,500}\"action\"\\s*:\\s*\"setup\"[\\s\\S]*"
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"x-envoy-upstream-service-time": [
|
||||
"590"
|
||||
],
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"traceresponse": [
|
||||
"00-f2a31e23c17caa48469882f5934cd46d-4f55b71500112ad2-01"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"set-cookie": [
|
||||
"_cfuvid=vx7p2XTKVCp8ymxCyY1Bf1uDntU.2lsVb5X3TtBBY8A-1777974553.6039832-1.0.1.1-m.FrPJVtR30V6V66YfGzigadwEH.6O1ClMIT7BTAU14; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CajDHmCLnzy9u6DgTCKTa"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-05-05T09:49:13Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"26975000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"27000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-05-05T09:49:13Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19998"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-05-05T09:49:13Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-05-05T09:49:13Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"22475000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"22500000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Tue, 05 May 2026 09:49:14 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"9f6ed5800ca3313f-SOF"
|
||||
]
|
||||
},
|
||||
"cookies": {
|
||||
"_cfuvid": "vx7p2XTKVCp8ymxCyY1Bf1uDntU.2lsVb5X3TtBBY8A-1777974553.6039832-1.0.1.1-m.FrPJVtR30V6V66YfGzigadwEH.6O1ClMIT7BTAU14"
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_01EsNoXS2Gk17WzZSyi3WX4T\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":539,\"cache_creation_input_tokens\":348,\"cache_read_input_tokens\":13705,\"cache_creation\":{\"ephemeral_5m_input_tokens\":348,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"The\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" workflow **\\\"B3 Workflow Setup Slack Credential Test\\\"** has been set up successfully. Here's a summary of what was configured:\\n\\n-\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" **Slack node** — connected to the credential **\\\"B3 Slack\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" Credential Test\\\"** (using access token authentication), configured to send a message to channel\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" `C01234567`.\\n\\nThe workflow is ready to use. Let me know if you'd like to test it or make any changes!\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":539,\"cache_creation_input_tokens\":348,\"cache_read_input_tokens\":13705,\"output_tokens\":101} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxRXNOb1hTMkdrMTdXelpTeWkzV1g0VCIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjUzOSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjozNDgsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjoxMzcwNSwiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MzQ4LCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6MSwic2VydmljZV90aWVyIjoic3RhbmRhcmQiLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIn19ICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdGFydApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0YXJ0IiwiaW5kZXgiOjAsImNvbnRlbnRfYmxvY2siOnsidHlwZSI6InRleHQiLCJ0ZXh0IjoiIn0gICAgfQoKZXZlbnQ6IHBpbmcKZGF0YTogeyJ0eXBlIjogInBpbmcifQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiJUaGUifSAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiB3b3JrZmxvdyAqKlwiQjMgV29ya2Zsb3cgU2V0dXAgU2xhY2sgQ3JlZGVudGlhbCBUZXN0XCIqKiBoYXMgYmVlbiBzZXQgdXAgc3VjY2Vzc2Z1bGx5LiBIZXJlJ3MgYSBzdW1tYXJ5IG9mIHdoYXQgd2FzIGNvbmZpZ3VyZWQ6XG5cbi0ifSAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiAqKlNsYWNrIG5vZGUqKiDigJQgY29ubmVjdGVkIHRvIHRoZSBjcmVkZW50aWFsICoqXCJCMyBTbGFjayJ9ICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiIENyZWRlbnRpYWwgVGVzdFwiKiogKHVzaW5nIGFjY2VzcyB0b2tlbiBhdXRoZW50aWNhdGlvbiksIGNvbmZpZ3VyZWQgdG8gc2VuZCBhIG1lc3NhZ2UgdG8gY2hhbm5lbCJ9ICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiBgQzAxMjM0NTY3YC5cblxuVGhlIHdvcmtmbG93IGlzIHJlYWR5IHRvIHVzZS4gTGV0IG1lIGtub3cgaWYgeW91J2QgbGlrZSB0byB0ZXN0IGl0IG9yIG1ha2UgYW55IGNoYW5nZXMhIn0gICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdG9wCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RvcCIsImluZGV4IjowICB9CgpldmVudDogbWVzc2FnZV9kZWx0YQpkYXRhOiB7InR5cGUiOiJtZXNzYWdlX2RlbHRhIiwiZGVsdGEiOnsic3RvcF9yZWFzb24iOiJlbmRfdHVybiIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInN0b3BfZGV0YWlscyI6bnVsbH0sInVzYWdlIjp7ImlucHV0X3Rva2VucyI6NTM5LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjM0OCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjEzNzA1LCJvdXRwdXRfdG9rZW5zIjoxMDF9ICAgfQoKZXZlbnQ6IG1lc3NhZ2Vfc3RvcApkYXRhOiB7InR5cGUiOiJtZXNzYWdlX3N0b3AiICAgICAgICAgfQoK",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "0002-1777974556275-unknown-host-POST-_v1_messages-97aea00c.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{"kind":"header","version":1,"testName":"recording","recordedAt":"2026-05-05T09:49:07.380Z"}
|
||||
{"kind":"tool-call","stepId":1,"agentRole":"orchestrator","toolName":"workflows","input":{"action":"list","query":"B3 Workflow Setup Slack Credential Test"},"output":{"workflows":[{"id":"fF6EAxQoY1E6TMjK","name":"B3 Workflow Setup Slack Credential Test","versionId":"fe8d3d23-dbd4-4088-9518-e61108c27c3c","activeVersionId":null,"createdAt":"2026-05-05T09:49:03.177Z","updatedAt":"2026-05-05T09:49:03.177Z"}]}}
|
||||
{"kind":"tool-call","stepId":2,"agentRole":"orchestrator","toolName":"workflows","input":{"action":"setup","workflowId":"fF6EAxQoY1E6TMjK"},"output":{"success":false}}
|
||||
{"kind":"tool-resume","stepId":3,"agentRole":"orchestrator","toolName":"workflows","input":{"action":"setup","workflowId":"fF6EAxQoY1E6TMjK"},"output":{"success":true,"completedNodes":[{"nodeName":"Slack","credentialType":"slackApi"}],"updatedNodes":[{"id":"trigger","name":"Manual Trigger","type":"n8n-nodes-base.manualTrigger","typeVersion":1,"position":[0,0],"parameters":{}},{"id":"slack","name":"Slack","type":"n8n-nodes-base.slack","typeVersion":2.2,"position":[220,0],"parameters":{"authentication":"accessToken","resource":"message","operation":"send","channelId":{"__rl":true,"mode":"id","value":"C01234567"},"messageType":"text","text":"Hello from instance AI workflow setup!"},"credentials":{"slackApi":{"id":"iR7AUjf5J6PiCmOb","name":"B3 Slack Credential Test"}}}],"updatedConnections":{"Manual Trigger":{"main":[[{"node":"Slack","type":"main","index":0}]]}}},"resumeData":{"approved":true,"credentials":{"Slack":{"slackApi":"iR7AUjf5J6PiCmOb"}},"action":"apply"}}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "[{\"type\":\"text\",\"text\":\"You are the n8n Instance Agent — an AI assistant embedde",
|
||||
"subString": true
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"x-envoy-upstream-service-time": [
|
||||
"1959"
|
||||
],
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"set-cookie": [
|
||||
"_cfuvid=1KDH2MR.suKQ3b0.cik0WD14ylcnLCsk4Bhdbe3YFYk-1777299224.7456198-1.0.1.1-h63Gud9zuljUvnQEfUsASs9Z54vlJiAQCLbtqNWJ3xE; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
|
||||
],
|
||||
"server-timing": [
|
||||
"x-originResponse;dur=1962"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CaUQzNJoRD9bpdJmLb78e"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-04-27T14:13:44Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"17987000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"18000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-04-27T14:13:44Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19998"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-04-27T14:13:44Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"3000000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"3000000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-04-27T14:13:45Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"14987000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"15000000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Mon, 27 Apr 2026 14:13:46 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"9f2e6dfaaa46313c-SOF"
|
||||
]
|
||||
},
|
||||
"cookies": {
|
||||
"_cfuvid": "1KDH2MR.suKQ3b0.cik0WD14ylcnLCsk4Bhdbe3YFYk-1777299224.7456198-1.0.1.1-h63Gud9zuljUvnQEfUsASs9Z54vlJiAQCLbtqNWJ3xE"
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_01X6vzwdYSbJqZrQZPDU6FCw\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":339,\"cache_creation_input_tokens\":10602,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":10602,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":45,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_01TotoiHcgF9vU5dm1bvk2KX\",\"name\":\"workflows\",\"input\":{},\"caller\":{\"type\":\"direct\"}} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"action\\\": \"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"list\\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\", \\\"query\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\": \\\"B3 W\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"ork\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"flow S\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"etup\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\" Apply Cred\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"entials\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"}\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":339,\"cache_creation_input_tokens\":10602,\"cache_read_input_tokens\":0,\"output_tokens\":77} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxWDZ2endkWVNiSnFaclFaUERVNkZDdyIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjMzOSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjoxMDYwMiwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjEwNjAyLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6NDUsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIiwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCJ9fSAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdGFydApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0YXJ0IiwiaW5kZXgiOjAsImNvbnRlbnRfYmxvY2siOnsidHlwZSI6InRvb2xfdXNlIiwiaWQiOiJ0b29sdV8wMVRvdG9pSGNnRjl2VTVkbTFidmsyS1giLCJuYW1lIjoid29ya2Zsb3dzIiwiaW5wdXQiOnt9LCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9fSAgICAgICB9CgpldmVudDogcGluZwpkYXRhOiB7InR5cGUiOiAicGluZyJ9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiIn0gICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoie1wiYWN0aW9uXCI6ICJ9ICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJcImxpc3RcIiJ9ICAgICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IiwgXCJxdWVyeSJ9ICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJcIjogXCJCMyBXIn0gICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoib3JrIn0gICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6ImZsb3cgUyJ9ICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6ImV0dXAifSAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiIgQXBwbHkgQ3JlZCJ9ICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJlbnRpYWxzIn0gICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJcIn0ifSB9CgpldmVudDogY29udGVudF9ibG9ja19zdG9wCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RvcCIsImluZGV4IjowICAgICAgICAgIH0KCmV2ZW50OiBtZXNzYWdlX2RlbHRhCmRhdGE6IHsidHlwZSI6Im1lc3NhZ2VfZGVsdGEiLCJkZWx0YSI6eyJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsfSwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjozMzksImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MTA2MDIsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJvdXRwdXRfdG9rZW5zIjo3N30gIH0KCmV2ZW50OiBtZXNzYWdlX3N0b3AKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdG9wIiAgIH0KCg==",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "0000-1777299238013-unknown-host-POST-_v1_messages-8a23f6c2.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "[{\"type\":\"text\",\"text\":\"You are the n8n Instance Agent — an AI assistant embedde",
|
||||
"subString": true
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"x-envoy-upstream-service-time": [
|
||||
"1341"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"set-cookie": [
|
||||
"_cfuvid=mJt0G.fjcBkfZwj_M0NEOa6u9SyH8jPnnXCTaKS76Os-1777299228.416283-1.0.1.1-4ph.ZluCTS2n1aD6gJPlO5Uuj9u4s9yegRSSw8QJpzI; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CaUQzdxdsUCM1pUKxHFNj"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-04-27T14:13:48Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"17987000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"18000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-04-27T14:13:48Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19998"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-04-27T14:13:48Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"3000000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"3000000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-04-27T14:13:48Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"14987000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"15000000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Mon, 27 Apr 2026 14:13:49 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"9f2e6e11988f9e0c-SOF"
|
||||
]
|
||||
},
|
||||
"cookies": {
|
||||
"_cfuvid": "mJt0G.fjcBkfZwj_M0NEOa6u9SyH8jPnnXCTaKS76Os-1777299228.416283-1.0.1.1-4ph.ZluCTS2n1aD6gJPlO5Uuj9u4s9yegRSSw8QJpzI"
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_01Y3oswJ9Pe1Uw7zWKTKhJXC\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":535,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":10602,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":51,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_01Mii4hvsHEjrZUfwPuaiATG\",\"name\":\"workflows\",\"input\":{},\"caller\":{\"type\":\"direct\"}} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"ac\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"tion\\\": \\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"setup\\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\", \\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"workflow\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"Id\\\": \\\"3\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"CpDPARR6\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"8X3ie\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"lq\\\"}\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":535,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":10602,\"output_tokens\":83} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxWTNvc3dKOVBlMVV3N3pXS1RLaEpYQyIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjUzNSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MTA2MDIsImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5zIjo1MSwic2VydmljZV90aWVyIjoic3RhbmRhcmQiLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIn19ICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdGFydApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0YXJ0IiwiaW5kZXgiOjAsImNvbnRlbnRfYmxvY2siOnsidHlwZSI6InRvb2xfdXNlIiwiaWQiOiJ0b29sdV8wMU1paTRodnNIRWpyWlVmd1B1YWlBVEciLCJuYW1lIjoid29ya2Zsb3dzIiwiaW5wdXQiOnt9LCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9fSB9CgpldmVudDogcGluZwpkYXRhOiB7InR5cGUiOiAicGluZyJ9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiIn0gICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IntcImFjIn0gICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJ0aW9uXCI6IFwiIn0gIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJzZXR1cFwiIn0gICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiLCBcIiJ9ICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IndvcmtmbG93In19CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiSWRcIjogXCIzIn0gICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiQ3BEUEFSUjYifSAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IjhYM2llIn0gICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoibHFcIn0ifSAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjAgICAgIH0KCmV2ZW50OiBtZXNzYWdlX2RlbHRhCmRhdGE6IHsidHlwZSI6Im1lc3NhZ2VfZGVsdGEiLCJkZWx0YSI6eyJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsfSwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjo1MzUsImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjEwNjAyLCJvdXRwdXRfdG9rZW5zIjo4M30gICAgICAgICAgICAgfQoKZXZlbnQ6IG1lc3NhZ2Vfc3RvcApkYXRhOiB7InR5cGUiOiJtZXNzYWdlX3N0b3AiICAgICAgfQoK",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "0001-1777299238014-unknown-host-POST-_v1_messages-8a23f6c2.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "[{\"type\":\"text\",\"text\":\"You are the n8n Instance Agent — an AI assistant embedde",
|
||||
"subString": true
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"x-envoy-upstream-service-time": [
|
||||
"1380"
|
||||
],
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"set-cookie": [
|
||||
"_cfuvid=OAtAhVpx.bRPrHAiZ8hDnSvoYd6eLb6NiDPgqpkRwYw-1777299233.9456034-1.0.1.1-OIilSrguVmoa_8IU7i5Csl38F41Hx0v.x7Cnsq25Vj0; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
|
||||
],
|
||||
"server-timing": [
|
||||
"x-originResponse;dur=1383"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CaUR13fQXufE2RaDtbTVe"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-04-27T14:13:54Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"17987000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"18000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-04-27T14:13:54Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19998"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-04-27T14:13:54Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"3000000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"3000000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-04-27T14:13:54Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"14987000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"15000000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Mon, 27 Apr 2026 14:13:55 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"9f2e6e342970dc1a-SOF"
|
||||
]
|
||||
},
|
||||
"cookies": {
|
||||
"_cfuvid": "OAtAhVpx.bRPrHAiZ8hDnSvoYd6eLb6NiDPgqpkRwYw-1777299233.9456034-1.0.1.1-OIilSrguVmoa_8IU7i5Csl38F41Hx0v.x7Cnsq25Vj0"
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_018fjjz7X8qzejn5PXWyJ5xd\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":1000,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":10602,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"The\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" workflow **\\\"B3 Workflow Setup Apply Credentials\\\"** has been set up successfully. Here's a summary of what was configured:\\n\\n- **HTTP\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" Request Basic** node — connected with the **\\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"B3 Apply Basic Auth\\\"** credential (HTTP Basic Auth).\\n- **HTTP Request Header** node — connected with the **\\\"B3 Apply Header Auth\\\"** credential (HTTP Header Auth).\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"\\n\\nBoth credentials were applied automatically. The workflow is ready to use — let\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" me know if you'd like to test or publish it!\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":1000,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":10602,\"output_tokens\":116} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxOGZqano3WDhxemVqbjVQWFd5SjV4ZCIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjEwMDAsImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjEwNjAyLCJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6MSwic2VydmljZV90aWVyIjoic3RhbmRhcmQiLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIn19ICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdGFydApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0YXJ0IiwiaW5kZXgiOjAsImNvbnRlbnRfYmxvY2siOnsidHlwZSI6InRleHQiLCJ0ZXh0IjoiIn0gfQoKZXZlbnQ6IHBpbmcKZGF0YTogeyJ0eXBlIjogInBpbmcifQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiJUaGUifSAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiB3b3JrZmxvdyAqKlwiQjMgV29ya2Zsb3cgU2V0dXAgQXBwbHkgQ3JlZGVudGlhbHNcIioqIGhhcyBiZWVuIHNldCB1cCBzdWNjZXNzZnVsbHkuIEhlcmUncyBhIHN1bW1hcnkgb2Ygd2hhdCB3YXMgY29uZmlndXJlZDpcblxuLSAqKkhUVFAifSAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgUmVxdWVzdCBCYXNpYyoqIG5vZGUg4oCUIGNvbm5lY3RlZCB3aXRoIHRoZSAqKlwiIn0gICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IkIzIEFwcGx5IEJhc2ljIEF1dGhcIioqIGNyZWRlbnRpYWwgKEhUVFAgQmFzaWMgQXV0aCkuXG4tICoqSFRUUCBSZXF1ZXN0IEhlYWRlcioqIG5vZGUg4oCUIGNvbm5lY3RlZCB3aXRoIHRoZSAqKlwiQjMgQXBwbHkgSGVhZGVyIEF1dGhcIioqIGNyZWRlbnRpYWwgKEhUVFAgSGVhZGVyIEF1dGgpLiJ9ICAgICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiJcblxuQm90aCBjcmVkZW50aWFscyB3ZXJlIGFwcGxpZWQgYXV0b21hdGljYWxseS4gVGhlIHdvcmtmbG93IGlzIHJlYWR5IHRvIHVzZSDigJQgbGV0In0gICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgbWUga25vdyBpZiB5b3UnZCBsaWtlIHRvIHRlc3Qgb3IgcHVibGlzaCBpdCEifSAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjAgICAgICAgICAgICAgIH0KCmV2ZW50OiBtZXNzYWdlX2RlbHRhCmRhdGE6IHsidHlwZSI6Im1lc3NhZ2VfZGVsdGEiLCJkZWx0YSI6eyJzdG9wX3JlYXNvbiI6ImVuZF90dXJuIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsfSwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjoxMDAwLCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjoxMDYwMiwib3V0cHV0X3Rva2VucyI6MTE2fSAgIH0KCmV2ZW50OiBtZXNzYWdlX3N0b3AKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdG9wIiAgICAgICAgICAgICB9Cgo=",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "0002-1777299238015-unknown-host-POST-_v1_messages-8a23f6c2.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{"kind":"header","version":1,"testName":"recording","recordedAt":"2026-04-27T14:13:42.821Z"}
|
||||
{"kind":"tool-call","stepId":1,"agentRole":"orchestrator","toolName":"workflows","input":{"action":"list","query":"B3 Workflow Setup Apply Credentials"},"output":{"workflows":[{"id":"3CpDPARR68X3ielq","name":"B3 Workflow Setup Apply Credentials","versionId":"48419b03-0714-46e8-965f-030f5611eb4e","activeVersionId":null,"createdAt":"2026-04-27T14:13:40.988Z","updatedAt":"2026-04-27T14:13:40.988Z"}]}}
|
||||
{"kind":"tool-call","stepId":2,"agentRole":"orchestrator","toolName":"workflows","input":{"action":"setup","workflowId":"3CpDPARR68X3ielq"},"output":{"success":false}}
|
||||
{"kind":"tool-resume","stepId":3,"agentRole":"orchestrator","toolName":"workflows","input":{"action":"setup","workflowId":"3CpDPARR68X3ielq"},"output":{"success":true,"completedNodes":[{"nodeName":"HTTP Request Basic","credentialType":"httpBasicAuth"},{"nodeName":"HTTP Request Header","credentialType":"httpHeaderAuth"}],"updatedNodes":[{"id":"trigger","name":"Manual Trigger","type":"n8n-nodes-base.manualTrigger","typeVersion":1,"position":[0,0],"parameters":{}},{"id":"basic","name":"HTTP Request Basic","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[220,0],"parameters":{"method":"GET","url":"https://example.com/basic","authentication":"genericCredentialType","genericAuthType":"httpBasicAuth"},"credentials":{"httpBasicAuth":{"id":"PBD5pMhDsNsHtRcg","name":"B3 Apply Basic Auth"}}},{"id":"header","name":"HTTP Request Header","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[440,0],"parameters":{"method":"GET","url":"https://example.com/header","authentication":"genericCredentialType","genericAuthType":"httpHeaderAuth"},"credentials":{"httpHeaderAuth":{"id":"GODaVzk969uA7AAo","name":"B3 Apply Header Auth"}}}],"updatedConnections":{"Manual Trigger":{"main":[[{"node":"HTTP Request Basic","type":"main","index":0}]]},"HTTP Request Basic":{"main":[[{"node":"HTTP Request Header","type":"main","index":0}]]}}},"resumeData":{"approved":true,"credentials":{"HTTP Request Basic":{"httpBasicAuth":"PBD5pMhDsNsHtRcg"},"HTTP Request Header":{"httpHeaderAuth":"GODaVzk969uA7AAo"}},"action":"apply"}}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "REGEX",
|
||||
"regex": "[\\s\\S]*\\[\\{\"type\":\"text\",\"text\":\"You are the n8n Instance Agent — an AI assistant embedde[\\s\\S]*Set up the workflow named \\\\\"B3 Workflow Setup Defer Credentials\\\\\"\\.[\\s\\S]*"
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"x-envoy-upstream-service-time": [
|
||||
"1078"
|
||||
],
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"traceresponse": [
|
||||
"00-3e487f75a38dc5f8a1e7682c970babd0-dc6d7edcaa9ba207-01"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"set-cookie": [
|
||||
"_cfuvid=6GAXic1opEKA6IHlLHZg_s2WU6lQM9KCHqd9Ly4dTTo-1777974466.5777943-1.0.1.1-6wF1aFN0NmwjTA1nohOdtmNxjMcKW3E3u8dXQoAmkZU; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
|
||||
],
|
||||
"server-timing": [
|
||||
"x-originResponse;dur=1101"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CajDBM2sKQMzZ82EFHMKF"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-05-05T09:47:46Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"26975000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"27000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-05-05T09:47:46Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19998"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-05-05T09:47:46Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-05-05T09:47:46Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"22475000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"22500000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Tue, 05 May 2026 09:47:47 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"9f6ed3601af3313c-SOF"
|
||||
]
|
||||
},
|
||||
"cookies": {
|
||||
"_cfuvid": "6GAXic1opEKA6IHlLHZg_s2WU6lQM9KCHqd9Ly4dTTo-1777974466.5777943-1.0.1.1-6wF1aFN0NmwjTA1nohOdtmNxjMcKW3E3u8dXQoAmkZU"
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_015G5aMpJRAyCpeHcv4QZzTY\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":340,\"cache_creation_input_tokens\":13705,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":13705,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Let\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" me look up that workflow first.\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_012AReqYbRXUt1kKA2w8kAbZ\",\"name\":\"workflows\",\"input\":{},\"caller\":{\"type\":\"direct\"}} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"act\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"ion\\\": \\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"li\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"st\\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\", \\\"query\\\": \\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"B3 Workfl\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"ow S\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"etup Defe\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"r Credentia\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"ls\\\"}\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":340,\"cache_creation_input_tokens\":13705,\"cache_read_input_tokens\":0,\"output_tokens\":86} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxNUc1YU1wSlJBeUNwZUhjdjRRWnpUWSIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjM0MCwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjoxMzcwNSwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjEzNzA1LCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6MSwic2VydmljZV90aWVyIjoic3RhbmRhcmQiLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIn19IH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0YXJ0CmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RhcnQiLCJpbmRleCI6MCwiY29udGVudF9ibG9jayI6eyJ0eXBlIjoidGV4dCIsInRleHQiOiIifSAgIH0KCmV2ZW50OiBwaW5nCmRhdGE6IHsidHlwZSI6ICJwaW5nIn0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiTGV0In0gICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgbWUgbG9vayB1cCB0aGF0IHdvcmtmbG93IGZpcnN0LiJ9ICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjAgICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdGFydApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0YXJ0IiwiaW5kZXgiOjEsImNvbnRlbnRfYmxvY2siOnsidHlwZSI6InRvb2xfdXNlIiwiaWQiOiJ0b29sdV8wMTJBUmVxWWJSWFV0MWtLQTJ3OGtBYloiLCJuYW1lIjoid29ya2Zsb3dzIiwiaW5wdXQiOnt9LCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9fSAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiIifSAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoie1wiIn0gICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6ImFjdCJ9ICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiaW9uXCI6IFwiIn0gICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJsaSJ9ICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJzdFwiIn0gICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiLCBcInF1ZXJ5XCI6IFwiIn0gICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiQjMgV29ya2ZsIn0gIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJvdyBTIn0gIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJldHVwIERlZmUifSAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiciBDcmVkZW50aWEifSAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6ImxzXCJ9In0gIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjEgICAgfQoKZXZlbnQ6IG1lc3NhZ2VfZGVsdGEKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9kZWx0YSIsImRlbHRhIjp7InN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJzdG9wX2RldGFpbHMiOm51bGx9LCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjM0MCwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjoxMzcwNSwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsIm91dHB1dF90b2tlbnMiOjg2fSAgIH0KCmV2ZW50OiBtZXNzYWdlX3N0b3AKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdG9wIiAgICAgICB9Cgo=",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "0000-1777974474128-unknown-host-POST-_v1_messages-b39899ad.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "REGEX",
|
||||
"regex": "[\\s\\S]*\\[\\{\"type\":\"text\",\"text\":\"You are the n8n Instance Agent — an AI assistant embedde[\\s\\S]*\"type\"\\s*:\\s*\"tool_use\"[\\s\\S]{0,300}\"name\"\\s*:\\s*\"workflows\"[\\s\\S]{0,500}\"action\"\\s*:\\s*\"list\"[\\s\\S]*"
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"x-envoy-upstream-service-time": [
|
||||
"1502"
|
||||
],
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"traceresponse": [
|
||||
"00-1085d60f4a362e7390c5feb6875bafd2-0ae3b110883d8b49-01"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"set-cookie": [
|
||||
"_cfuvid=vCPFHZtvW_DRxETwdACxZadvCNzkrRlANYljYk5qYIM-1777974468.848351-1.0.1.1-ADUWIW9UEzyJPTgV8mky5rCF11LzQ3q7RcfNnnzQt28; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CajDBWqivYggYoCGkZt43"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-05-05T09:47:49Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"26975000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"27000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-05-05T09:47:49Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19998"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-05-05T09:47:49Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-05-05T09:47:49Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"22475000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"22500000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Tue, 05 May 2026 09:47:50 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"9f6ed36e4d304d27-SOF"
|
||||
]
|
||||
},
|
||||
"cookies": {
|
||||
"_cfuvid": "vCPFHZtvW_DRxETwdACxZadvCNzkrRlANYljYk5qYIM-1777974468.848351-1.0.1.1-ADUWIW9UEzyJPTgV8mky5rCF11LzQ3q7RcfNnnzQt28"
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_013DrvEBkXjQY7kj61xBd6Fm\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":195,\"cache_creation_input_tokens\":14053,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":14053,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":67,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_013C6mmLw13ebcMsZAhtXA2T\",\"name\":\"workflows\",\"input\":{},\"caller\":{\"type\":\"direct\"}} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"action\\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\": \\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"set\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"up\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\", \\\"wor\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"kflowI\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"d\\\": \\\"TXSVC\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"xTExNA9UskZ\\\"}\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":195,\"cache_creation_input_tokens\":14053,\"cache_read_input_tokens\":0,\"output_tokens\":83} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxM0RydkVCa1hqUVk3a2o2MXhCZDZGbSIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjE5NSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjoxNDA1MywiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjE0MDUzLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6NjcsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIiwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCJ9fSAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdGFydApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0YXJ0IiwiaW5kZXgiOjAsImNvbnRlbnRfYmxvY2siOnsidHlwZSI6InRvb2xfdXNlIiwiaWQiOiJ0b29sdV8wMTNDNm1tTHcxM2ViY01zWkFodFhBMlQiLCJuYW1lIjoid29ya2Zsb3dzIiwiaW5wdXQiOnt9LCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9fSAgfQoKZXZlbnQ6IHBpbmcKZGF0YTogeyJ0eXBlIjogInBpbmcifQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IiJ9ICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJ7XCJhY3Rpb25cIiJ9ICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiI6IFwiIn0gICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJzZXQifSAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJ1cCJ9ICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IlwiIn0gICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6IiwgXCJ3b3IifSAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoia2Zsb3dJIn0gICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiZFwiOiBcIlRYU1ZDIn0gICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJ4VEV4TkE5VXNrWlwifSJ9ICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdG9wCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RvcCIsImluZGV4IjowIH0KCmV2ZW50OiBtZXNzYWdlX2RlbHRhCmRhdGE6IHsidHlwZSI6Im1lc3NhZ2VfZGVsdGEiLCJkZWx0YSI6eyJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsfSwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjoxOTUsImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MTQwNTMsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJvdXRwdXRfdG9rZW5zIjo4M30gIH0KCmV2ZW50OiBtZXNzYWdlX3N0b3AKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdG9wIiAgICAgfQoK",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "0001-1777974474129-unknown-host-POST-_v1_messages-c17e02f9.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "REGEX",
|
||||
"regex": "[\\s\\S]*\\[\\{\"type\":\"text\",\"text\":\"You are the n8n Instance Agent — an AI assistant embedde[\\s\\S]*\"type\"\\s*:\\s*\"tool_use\"[\\s\\S]{0,300}\"name\"\\s*:\\s*\"workflows\"[\\s\\S]{0,500}\"action\"\\s*:\\s*\"setup\"[\\s\\S]*"
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"x-envoy-upstream-service-time": [
|
||||
"1091"
|
||||
],
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"traceresponse": [
|
||||
"00-63a0d00446f0c08b48aff3c44201bb0c-1fcd42c6541d8efa-01"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"set-cookie": [
|
||||
"_cfuvid=xaTD7jfqz3st54WpwpJTateVwJDO5mSgcZtKWlBgtIs-1777974471.5852163-1.0.1.1-9QsKC0UXcBLahIeqWuJIlqDjFMI9qQi3.2EJ44ujngw; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
|
||||
],
|
||||
"server-timing": [
|
||||
"x-originResponse;dur=1107"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CajDBiSEFtLXpdg4kYfYn"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-05-05T09:47:51Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"26975000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"27000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-05-05T09:47:51Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19998"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-05-05T09:47:51Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-05-05T09:47:51Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"22475000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"22500000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Tue, 05 May 2026 09:47:52 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"9f6ed37f6a95dc1a-SOF"
|
||||
]
|
||||
},
|
||||
"cookies": {
|
||||
"_cfuvid": "xaTD7jfqz3st54WpwpJTateVwJDO5mSgcZtKWlBgtIs-1777974471.5852163-1.0.1.1-9QsKC0UXcBLahIeqWuJIlqDjFMI9qQi3.2EJ44ujngw"
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_015ea3zyDkR9JA5w5mHjYP4j\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":310,\"cache_creation_input_tokens\":348,\"cache_read_input_tokens\":13705,\"cache_creation\":{\"ephemeral_5m_input_tokens\":348,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"The\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" setup was deferred — you chose to skip credential and parameter configuration for now.\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" Whenever you're ready to set it up, just let me know and I'll open the setup wizard again.\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":310,\"cache_creation_input_tokens\":348,\"cache_read_input_tokens\":13705,\"output_tokens\":43} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxNWVhM3p5RGtSOUpBNXc1bUhqWVA0aiIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjMxMCwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjozNDgsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjoxMzcwNSwiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MzQ4LCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6MSwic2VydmljZV90aWVyIjoic3RhbmRhcmQiLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIn19ICB9CgpldmVudDogY29udGVudF9ibG9ja19zdGFydApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0YXJ0IiwiaW5kZXgiOjAsImNvbnRlbnRfYmxvY2siOnsidHlwZSI6InRleHQiLCJ0ZXh0IjoiIn0gICAgICAgIH0KCmV2ZW50OiBwaW5nCmRhdGE6IHsidHlwZSI6ICJwaW5nIn0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiVGhlIn0gICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgc2V0dXAgd2FzIGRlZmVycmVkIOKAlCB5b3UgY2hvc2UgdG8gc2tpcCBjcmVkZW50aWFsIGFuZCBwYXJhbWV0ZXIgY29uZmlndXJhdGlvbiBmb3Igbm93LiJ9ICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgV2hlbmV2ZXIgeW91J3JlIHJlYWR5IHRvIHNldCBpdCB1cCwganVzdCBsZXQgbWUga25vdyBhbmQgSSdsbCBvcGVuIHRoZSBzZXR1cCB3aXphcmQgYWdhaW4uIn0gICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfc3RvcApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0b3AiLCJpbmRleCI6MCAgfQoKZXZlbnQ6IG1lc3NhZ2VfZGVsdGEKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9kZWx0YSIsImRlbHRhIjp7InN0b3BfcmVhc29uIjoiZW5kX3R1cm4iLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJzdG9wX2RldGFpbHMiOm51bGx9LCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjMxMCwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjozNDgsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjoxMzcwNSwib3V0cHV0X3Rva2VucyI6NDN9ICAgIH0KCmV2ZW50OiBtZXNzYWdlX3N0b3AKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdG9wIn0KCg==",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "0002-1777974474129-unknown-host-POST-_v1_messages-97aea00c.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{"kind":"header","version":1,"testName":"recording","recordedAt":"2026-05-05T09:47:46.281Z"}
|
||||
{"kind":"tool-call","stepId":1,"agentRole":"orchestrator","toolName":"workflows","input":{"action":"list","query":"B3 Workflow Setup Defer Credentials"},"output":{"workflows":[{"id":"TXSVCxTExNA9UskZ","name":"B3 Workflow Setup Defer Credentials","versionId":"5b012327-ac57-4188-885e-238d30843694","activeVersionId":null,"createdAt":"2026-05-05T09:47:42.087Z","updatedAt":"2026-05-05T09:47:42.087Z"}]}}
|
||||
{"kind":"tool-call","stepId":2,"agentRole":"orchestrator","toolName":"workflows","input":{"action":"setup","workflowId":"TXSVCxTExNA9UskZ"},"output":{"success":false}}
|
||||
{"kind":"tool-resume","stepId":3,"agentRole":"orchestrator","toolName":"workflows","input":{"action":"setup","workflowId":"TXSVCxTExNA9UskZ"},"output":{"success":true,"deferred":true,"reason":"User skipped workflow setup for now."},"resumeData":{"approved":false}}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
{
|
||||
"httpRequest": {
|
||||
"method": "POST",
|
||||
"path": "/v1/messages",
|
||||
"body": {
|
||||
"type": "REGEX",
|
||||
"regex": "[\\s\\S]*\\[\\{\"type\":\"text\",\"text\":\"You are the n8n Instance Agent — an AI assistant embedde[\\s\\S]*Set up the workflow named \\\\\"B3 Workflow Setup Apply Credentials\\\\\"\\.[\\s\\S]*"
|
||||
}
|
||||
},
|
||||
"httpResponse": {
|
||||
"statusCode": 200,
|
||||
"reasonPhrase": "OK",
|
||||
"headers": {
|
||||
"x-envoy-upstream-service-time": [
|
||||
"1052"
|
||||
],
|
||||
"vary": [
|
||||
"Accept-Encoding"
|
||||
],
|
||||
"traceresponse": [
|
||||
"00-2668ca2226c99a70ec74890b0f4bcc29-4bc9a6b3b1d55bbc-01"
|
||||
],
|
||||
"strict-transport-security": [
|
||||
"max-age=31536000; includeSubDomains; preload"
|
||||
],
|
||||
"set-cookie": [
|
||||
"_cfuvid=dv8sDGFw6P1PZ_BtIoA3IfN1.6gDBSxPYbT3wiHls7Q-1777974439.1522524-1.0.1.1-uJZxg5J7MJDm67DswPe8FpgfB9Y7CTs6pcTOMb2HJYM; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
|
||||
],
|
||||
"server-timing": [
|
||||
"x-originResponse;dur=1055"
|
||||
],
|
||||
"request-id": [
|
||||
"req_011CajD9KmueJ78rX83WbYeo"
|
||||
],
|
||||
"cf-cache-status": [
|
||||
"DYNAMIC"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-reset": [
|
||||
"2026-05-05T09:47:19Z"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-remaining": [
|
||||
"26975000"
|
||||
],
|
||||
"anthropic-ratelimit-tokens-limit": [
|
||||
"27000000"
|
||||
],
|
||||
"anthropic-ratelimit-requests-reset": [
|
||||
"2026-05-05T09:47:19Z"
|
||||
],
|
||||
"anthropic-ratelimit-requests-remaining": [
|
||||
"19998"
|
||||
],
|
||||
"anthropic-ratelimit-requests-limit": [
|
||||
"20000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-reset": [
|
||||
"2026-05-05T09:47:19Z"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-remaining": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-output-tokens-limit": [
|
||||
"4500000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-reset": [
|
||||
"2026-05-05T09:47:19Z"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-remaining": [
|
||||
"22475000"
|
||||
],
|
||||
"anthropic-ratelimit-input-tokens-limit": [
|
||||
"22500000"
|
||||
],
|
||||
"X-Robots-Tag": [
|
||||
"none"
|
||||
],
|
||||
"Server": [
|
||||
"cloudflare"
|
||||
],
|
||||
"Date": [
|
||||
"Tue, 05 May 2026 09:47:20 GMT"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/event-stream; charset=utf-8"
|
||||
],
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'none'; frame-ancestors 'none'"
|
||||
],
|
||||
"Cache-Control": [
|
||||
"no-cache"
|
||||
],
|
||||
"CF-RAY": [
|
||||
"9f6ed2b4bdd54d27-SOF"
|
||||
]
|
||||
},
|
||||
"cookies": {
|
||||
"_cfuvid": "dv8sDGFw6P1PZ_BtIoA3IfN1.6gDBSxPYbT3wiHls7Q-1777974439.1522524-1.0.1.1-uJZxg5J7MJDm67DswPe8FpgfB9Y7CTs6pcTOMb2HJYM"
|
||||
},
|
||||
"body": {
|
||||
"type": "STRING",
|
||||
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_016wuAtv2VDFXyDLdgZ5KKye\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":339,\"cache_creation_input_tokens\":13705,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":13705,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"I\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"'ll look up that workflow right away.\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_01V2BzMfTbgkRZf2G9gXsmTe\",\"name\":\"workflows\",\"input\":{},\"caller\":{\"type\":\"direct\"}} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"act\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"ion\\\": \\\"list\\\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\", \\\"query\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\": \\\"B3 Work\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"flow Setup\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\" Apply Crede\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"ntials\\\"}\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":339,\"cache_creation_input_tokens\":13705,\"cache_read_input_tokens\":0,\"output_tokens\":86} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
|
||||
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNiIsImlkIjoibXNnXzAxNnd1QXR2MlZERlh5RExkZ1o1S0t5ZSIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjMzOSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjoxMzcwNSwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjEzNzA1LCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6MSwic2VydmljZV90aWVyIjoic3RhbmRhcmQiLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIn19ICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0YXJ0CmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RhcnQiLCJpbmRleCI6MCwiY29udGVudF9ibG9jayI6eyJ0eXBlIjoidGV4dCIsInRleHQiOiIifSAgICAgICAgICB9CgpldmVudDogcGluZwpkYXRhOiB7InR5cGUiOiAicGluZyJ9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IkkifSAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiInbGwgbG9vayB1cCB0aGF0IHdvcmtmbG93IHJpZ2h0IGF3YXkuIn0gICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0YXJ0CmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RhcnQiLCJpbmRleCI6MSwiY29udGVudF9ibG9jayI6eyJ0eXBlIjoidG9vbF91c2UiLCJpZCI6InRvb2x1XzAxVjJCek1mVGJna1JaZjJHOWdYc21UZSIsIm5hbWUiOiJ3b3JrZmxvd3MiLCJpbnB1dCI6e30sImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In19ICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiIn0gICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJ7XCJhY3QifSAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6ImlvblwiOiBcImxpc3RcIiJ9ICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiIsIFwicXVlcnkifSAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiJcIjogXCJCMyBXb3JrIn0gICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJpbnB1dF9qc29uX2RlbHRhIiwicGFydGlhbF9qc29uIjoiZmxvdyBTZXR1cCJ9ICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6ImlucHV0X2pzb25fZGVsdGEiLCJwYXJ0aWFsX2pzb24iOiIgQXBwbHkgQ3JlZGUifSAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoiaW5wdXRfanNvbl9kZWx0YSIsInBhcnRpYWxfanNvbiI6Im50aWFsc1wifSJ9ICAgICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfc3RvcApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0b3AiLCJpbmRleCI6MSAgICAgICAgICB9CgpldmVudDogbWVzc2FnZV9kZWx0YQpkYXRhOiB7InR5cGUiOiJtZXNzYWdlX2RlbHRhIiwiZGVsdGEiOnsic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInN0b3BfZGV0YWlscyI6bnVsbH0sInVzYWdlIjp7ImlucHV0X3Rva2VucyI6MzM5LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjEzNzA1LCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwib3V0cHV0X3Rva2VucyI6ODZ9ICAgIH0KCmV2ZW50OiBtZXNzYWdlX3N0b3AKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdG9wIiAgIH0KCg==",
|
||||
"contentType": "text/event-stream; charset=utf-8"
|
||||
}
|
||||
},
|
||||
"id": "0000-1777974459341-unknown-host-POST-_v1_messages-43762f74.json",
|
||||
"priority": 0,
|
||||
"timeToLive": {
|
||||
"unlimited": true
|
||||
},
|
||||
"times": {
|
||||
"unlimited": true
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user