可运行实验 · 真实 n8n 引擎的 JSON 兜底
在 n8n 2.44.7 的断网临时容器里执行 Manual Trigger → 固定模型输出 → Validate → IF → 成功或兜底。包含合法 JSON、损坏 JSON、缺字段三条输入。另附可导入的 Webhook → 模型 HTTP 请求 → 同一验证逻辑 → 回应工作流。
环境、下载包与验证范围见 6.1。
依赖:Python 3.11 标准库、Docker、n8n==2.44.7。先启动 Docker,再下载镜像:
docker pull docker.n8n.io/n8nio/n8n:2.44.7
运行命令与预期输出
python3.11 s66_n8n.py
n8n 2.44.7:真实工作流;容器断网;模型输出为固定样本
valid → success:学习站提供分阶段课程。
malformed → fallback:生成结果格式错误,请稍后重试。
missing-field → fallback:生成结果格式错误,请稍后重试。
3/3 分支验收通过;容器退出即删除数据库
验收:真实 n8n 执行的三条输入分别进入 success、fallback、fallback。只读挂载资料,退出/超时后清理本次容器;没有向外发送通知。在线 workflow 尚需绑定凭据并完成 webhook 端到端验收。
常见报错与处理
- Cannot connect to Docker daemon:先启动 Docker Desktop,再 pull 固定镜像。
- Mounts denied / timeout:允许 Docker 访问解压目录,检查资源;启动器最长等待 120 秒。
- Python task runner missing:此例只使用 JS Code 节点;验证 JS Task Runner 正常注册。
- Webhook 404 / 模型 401:按 N8N.md 开启测试监听或发布,绑定 Header Auth;不把密钥写入工作流 JSON。
接口依据:官方文档;具体行为以本例固定版本实测为准。
完整代码与配套文件
下列内容来自实际文件;运行时使用完整下载包,保留共享模块、资料与工作流目录。
s66_n8n.py
打开文件
"""在断网的临时 n8n 容器中实际执行工作流,并检查成功/失败路由。"""
import json
from pathlib import Path
import subprocess
import uuid
IMAGE = 'docker.n8n.io/n8nio/n8n:2.44.7'
ROOT = Path(__file__).resolve().parent
def parse_execution(output):
# n8n --rawOutput 前仍可能打印迁移与 runner 日志。
decoder = json.JSONDecoder()
for i, char in enumerate(output):
if char != '{':
continue
try:
value, _ = decoder.raw_decode(output[i:])
except ValueError:
continue
if isinstance(value, dict) and 'data' in value and 'finished' in value:
return value
raise RuntimeError('没有找到 n8n 执行结果;检查容器日志')
def verify(result):
if result.get('status') != 'success':
raise RuntimeError('工作流失败:' + str(result.get('status')))
runs = result['data']['resultData']['runData']
items = [item['json'] for name in ('Success', 'Fallback')
for run in runs.get(name, []) for branch in run['data']['main'] for item in branch]
routes = {i['case']: i['route'] for i in items}
if routes != {'valid': 'success', 'malformed': 'fallback', 'missing-field': 'fallback'}:
raise RuntimeError('分支结果与验收不符:' + str(routes))
return items
def main():
name = 'stage6-test-' + uuid.uuid4().hex[:12]
cmd = ['docker', 'run', '--rm', '--name', name, '--network', 'none',
'-e', 'N8N_DIAGNOSTICS_ENABLED=false', '-e', 'N8N_VERSION_NOTIFICATIONS_ENABLED=false',
'-e', 'N8N_TEMPLATES_ENABLED=false', '-e', 'N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true',
'-v', f'{ROOT / "n8n"}:/work:ro', '--entrypoint', '/bin/sh', IMAGE, '-c',
'n8n import:workflow --input=/work/offline.json >/tmp/import.log && '
'n8n execute --id=Stage6Smoke --rawOutput']
try:
result = subprocess.run(cmd, text=True, capture_output=True, timeout=120)
finally:
# 超时杀掉 Docker CLI 后也清理本次容器;不会操作其他容器。
subprocess.run(['docker', 'rm', '-f', name], capture_output=True, timeout=15)
if result.returncode:
raise RuntimeError('n8n 执行失败:\n' + (result.stderr + result.stdout)[-2000:])
items = verify(parse_execution(result.stdout))
print('n8n 2.44.7:真实工作流;容器断网;模型输出为固定样本')
for item in items:
print(f'{item["case"]} → {item["route"]}:{item["summary"]}')
print('3/3 分支验收通过;容器退出即删除数据库')
if __name__ == '__main__':
try:
main()
except (RuntimeError, FileNotFoundError, subprocess.TimeoutExpired) as exc:
raise SystemExit(str(exc))
n8n/validate.js
打开文件
return $input.all().map(item => {
let raw = item.json.modelText;
if (raw === undefined) raw = (item.json.output || []).flatMap(x => x.content || []).filter(x => x.type === 'output_text').map(x => x.text).join('');
try {
const value = JSON.parse(raw);
if (!value || typeof value.summary !== 'string' || !value.summary.trim() || value.summary.length > 1000) throw new Error('summary must be a nonempty string <= 1000 chars');
return {json: {case: item.json.case || 'webhook', ok: true, summary: value.summary}};
} catch (_) {
return {json: {case: item.json.case || 'webhook', ok: false, summary: '生成结果格式错误,请稍后重试。'}};
}
});
n8n/offline.json
打开文件
{
"id": "Stage6Smoke",
"name": "Stage6 Offline JSON Guard",
"active": false,
"nodes": [
{
"id": "Start",
"name": "Start",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
0,
0
],
"parameters": {}
},
{
"id": "Fixture",
"name": "Fixture",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
240,
0
],
"parameters": {
"jsCode": "return [{json:{case:'valid',modelText:'{\"summary\":\"学习站提供分阶段课程。\"}'}},{json:{case:'malformed',modelText:'{not json'}},{json:{case:'missing-field',modelText:'{\"text\":\"oops\"}'}}];"
}
},
{
"id": "Validate",
"name": "Validate",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
480,
0
],
"parameters": {
"jsCode": "return $input.all().map(item => {\n let raw = item.json.modelText;\n if (raw === undefined) raw = (item.json.output || []).flatMap(x => x.content || []).filter(x => x.type === 'output_text').map(x => x.text).join('');\n try {\n const value = JSON.parse(raw);\n if (!value || typeof value.summary !== 'string' || !value.summary.trim() || value.summary.length > 1000) throw new Error('summary must be a nonempty string <= 1000 chars');\n return {json: {case: item.json.case || 'webhook', ok: true, summary: value.summary}};\n } catch (_) {\n return {json: {case: item.json.case || 'webhook', ok: false, summary: '生成结果格式错误,请稍后重试。'}};\n }\n});"
}
},
{
"id": "Valid?",
"name": "Valid?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
720,
0
],
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "valid",
"leftValue": "={{ $json.ok }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
}
},
{
"id": "Success",
"name": "Success",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
960,
-100
],
"parameters": {
"jsCode": "return $input.all().map(x => ({json: {...x.json, route: 'success'}}));"
}
},
{
"id": "Fallback",
"name": "Fallback",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
960,
100
],
"parameters": {
"jsCode": "return $input.all().map(x => ({json: {...x.json, route: 'fallback'}}));"
}
}
],
"connections": {
"Validate": {
"main": [
[
{
"node": "Valid?",
"type": "main",
"index": 0
}
]
]
},
"Valid?": {
"main": [
[
{
"node": "Success",
"type": "main",
"index": 0
}
],
[
{
"node": "Fallback",
"type": "main",
"index": 0
}
]
]
},
"Start": {
"main": [
[
{
"node": "Fixture",
"type": "main",
"index": 0
}
]
]
},
"Fixture": {
"main": [
[
{
"node": "Validate",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
n8n/webhook.json
打开文件
{
"name": "Stage6 Webhook Summary",
"active": false,
"nodes": [
{
"id": "Webhook",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
0
],
"parameters": {
"httpMethod": "POST",
"path": "stage6-summary",
"responseMode": "responseNode",
"options": {}
},
"webhookId": "stage6-summary"
},
{
"id": "Model",
"name": "Model",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
240,
0
],
"parameters": {
"method": "POST",
"url": "https://api.openai.com/v1/responses",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({model: \"gpt-5.4-mini-2026-03-17\", input: \"Return JSON only with one string field summary. Summarize: \" + String($json.body.text || \"\"), max_output_tokens: 256, store: false}) }}",
"options": {
"timeout": 30000
}
},
"onError": "continueRegularOutput"
},
{
"id": "Validate",
"name": "Validate",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
480,
0
],
"parameters": {
"jsCode": "return $input.all().map(item => {\n let raw = item.json.modelText;\n if (raw === undefined) raw = (item.json.output || []).flatMap(x => x.content || []).filter(x => x.type === 'output_text').map(x => x.text).join('');\n try {\n const value = JSON.parse(raw);\n if (!value || typeof value.summary !== 'string' || !value.summary.trim() || value.summary.length > 1000) throw new Error('summary must be a nonempty string <= 1000 chars');\n return {json: {case: item.json.case || 'webhook', ok: true, summary: value.summary}};\n } catch (_) {\n return {json: {case: item.json.case || 'webhook', ok: false, summary: '生成结果格式错误,请稍后重试。'}};\n }\n});"
}
},
{
"id": "Valid?",
"name": "Valid?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
720,
0
],
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "valid",
"leftValue": "={{ $json.ok }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
}
},
{
"id": "Success",
"name": "Success",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
960,
-100
],
"parameters": {
"jsCode": "return $input.all().map(x => ({json: {...x.json, route: 'success'}}));"
}
},
{
"id": "Fallback",
"name": "Fallback",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
960,
100
],
"parameters": {
"jsCode": "return $input.all().map(x => ({json: {...x.json, route: 'fallback'}}));"
}
},
{
"id": "Reply",
"name": "Reply",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
1200,
0
],
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify($json) }}",
"options": {}
}
}
],
"connections": {
"Validate": {
"main": [
[
{
"node": "Valid?",
"type": "main",
"index": 0
}
]
]
},
"Valid?": {
"main": [
[
{
"node": "Success",
"type": "main",
"index": 0
}
],
[
{
"node": "Fallback",
"type": "main",
"index": 0
}
]
]
},
"Webhook": {
"main": [
[
{
"node": "Model",
"type": "main",
"index": 0
}
]
]
},
"Model": {
"main": [
[
{
"node": "Validate",
"type": "main",
"index": 0
}
]
]
},
"Success": {
"main": [
[
{
"node": "Reply",
"type": "main",
"index": 0
}
]
]
},
"Fallback": {
"main": [
[
{
"node": "Reply",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
N8N.md
打开文件
# 6.6 n8n 2.44.7
Python 启动器只用标准库;安装并启动 Docker Desktop,然后运行:
```sh
docker pull docker.n8n.io/n8nio/n8n:2.44.7
python s66_n8n.py
```
实测镜像 digest:`sha256:942bc274709f27f99f7c6024a785d8e8f75ac06838cabcba448abb8d9127ec7b`。可使用 `docker.n8n.io/n8nio/n8n@sha256:…` 固定同一镜像。版本标签可跨平台拉取;本次在 macOS Apple Silicon / Docker Server 29.5.3 运行。
`n8n/offline.json` 在真实 n8n 引擎中执行 Manual Trigger → Fixture → Validate → IF → Success/Fallback。三条样本为合法 JSON、损坏 JSON、缺少 summary。Validate 不会因 JSON.parse 报错终止整个任务,IF 选择友好兜底。容器没有网络,资料只读挂载,数据库不持久化,不发送通知。
## Webhook 与真实模型
`n8n/webhook.json` 是可导入的在线版本:Webhook → Model(HTTP Request)→ Validate → IF → Reply。Validate/IF 与离线版本完全相同。在线版已做结构检查,尚未在已发布 webhook 上端到端验证。
1. 在 n8n 2.44.7 的编辑器 Import from File,导入 webhook.json。
2. Model 节点选择 Header Auth 凭据;Header 名为 `Authorization`,值为 `Bearer 你的模型密钥`。凭据保存在 n8n 凭据管理器,不写进 JSON。模型默认 `gpt-5.4-mini-2026-03-17`,可换为账号有权限且支持 Responses 的模型。
3. 点击 Webhook 的 Listen for test event。复制节点显示的 Test URL(仅测试监听期间有效),以 JSON 发送 `{"text":"学习站提供分阶段课程和本地进度。"}`。
```sh
curl -X POST 'http://localhost:5678/webhook-test/stage6-summary' \
-H 'Content-Type: application/json' \
-d '{"text":"学习站提供分阶段课程和本地进度。"}'
```
4. 正常应返回 `ok:true`、非空 summary、`route:"success"`;Model 节点请求失败、非 JSON 或缺字段时返回 `ok:false`、友好提示及 `route:"fallback"`。响应措辞不固定。检查执行记录确认失败发生的位置。
5. 需要长期使用才发布,并使用 Production URL;对外暴露前给 Webhook 配置认证。示例只回应请求,不接通知服务。
默认最多一次模型 HTTP 请求,30 秒超时,256 输出 tokens;这不是费用上限。在线模式会把输入发给模型服务。当前 prompt 要求 JSON 但不保证模型遵守,所以仍保留验证与兜底。
## 常见报错
- Docker daemon 无法连接:启动 Docker Desktop;确认当前用户可访问 Docker。
- Mounts denied:允许 Docker 访问解压目录;完整解压,不要只复制启动器。
- 镜像不存在:先 pull 固定版本;离线容器不负责下载模型。
- 120 秒超时:检查 Docker 资源和启动日志;不要把超时改成无限。
- 401:Model 节点没有绑定 Header Auth 或密钥无效。工作流仍应进入 fallback。
- Webhook 404:测试 URL 需要监听;生产 URL 需要发布。
- 编辑器节点类型不支持:确认 n8n 2.44.7,旧版本不在本次验证范围。
官方 CLI:https://docs.n8n.io/hosting/cli-commands/