feat(n8n): wire notify and persist append logs
This commit is contained in:
@@ -6,12 +6,18 @@ from pathlib import Path
|
||||
REQUIRED_NODE_NAMES = {
|
||||
'Webhook',
|
||||
'route-action',
|
||||
'route-dispatch',
|
||||
'Send Telegram Notification',
|
||||
'Send Discord Notification',
|
||||
'Respond to Webhook',
|
||||
}
|
||||
|
||||
EXPECTED_DIRECT_TYPES = {
|
||||
EXPECTED_TYPES = {
|
||||
'Webhook': 'n8n-nodes-base.webhook',
|
||||
'route-action': 'n8n-nodes-base.code',
|
||||
'route-dispatch': 'n8n-nodes-base.switch',
|
||||
'Send Telegram Notification': 'n8n-nodes-base.telegram',
|
||||
'Send Discord Notification': 'n8n-nodes-base.httpRequest',
|
||||
'Respond to Webhook': 'n8n-nodes-base.respondToWebhook',
|
||||
}
|
||||
|
||||
@@ -31,10 +37,6 @@ def load_json(path: Path):
|
||||
def main():
|
||||
path = Path(sys.argv[1]) if len(sys.argv) > 1 else Path('assets/openclaw-action.workflow.json')
|
||||
data = load_json(path)
|
||||
|
||||
if not isinstance(data, dict):
|
||||
fail('workflow file must decode to a JSON object')
|
||||
|
||||
nodes = data.get('nodes')
|
||||
connections = data.get('connections')
|
||||
if not isinstance(nodes, list):
|
||||
@@ -55,55 +57,52 @@ def main():
|
||||
if missing:
|
||||
fail(f'missing required nodes: {", ".join(missing)}')
|
||||
|
||||
for name, node_type in EXPECTED_DIRECT_TYPES.items():
|
||||
for name, node_type in EXPECTED_TYPES.items():
|
||||
actual = by_name[name].get('type')
|
||||
if actual != node_type:
|
||||
fail(f'node {name!r} should have type {node_type!r}, got {actual!r}')
|
||||
|
||||
webhook = by_name['Webhook']
|
||||
webhook_params = webhook.get('parameters', {})
|
||||
if webhook_params.get('path') != 'openclaw-action':
|
||||
webhook = by_name['Webhook'].get('parameters', {})
|
||||
if webhook.get('path') != 'openclaw-action':
|
||||
fail('Webhook.path must be openclaw-action')
|
||||
if webhook_params.get('httpMethod') != 'POST':
|
||||
if webhook.get('httpMethod') != 'POST':
|
||||
fail('Webhook.httpMethod must be POST')
|
||||
if webhook_params.get('responseMode') != 'responseNode':
|
||||
if webhook.get('responseMode') != 'responseNode':
|
||||
fail('Webhook.responseMode must be responseNode')
|
||||
|
||||
router = by_name['route-action'].get('parameters', {})
|
||||
if router.get('mode') != 'runOnceForEachItem':
|
||||
fail('route-action code node must use runOnceForEachItem mode')
|
||||
if router.get('language') != 'javaScript':
|
||||
fail('route-action code node must use javaScript language')
|
||||
js_code = router.get('jsCode', '')
|
||||
for snippet in ("append_log", "notify", "unknown_action", "invalid_request", "status_code", "response_body"):
|
||||
for snippet in ('append_log', 'notify', 'unknown_action', 'invalid_request', '$getWorkflowStaticData', 'actionLog', 'retained_entries', 'notify_text'):
|
||||
if snippet not in js_code:
|
||||
fail(f'route-action jsCode missing expected snippet: {snippet!r}')
|
||||
|
||||
route_outputs = connections.get('route-action', {}).get('main', [])
|
||||
if len(route_outputs) < 1:
|
||||
fail('route-action must connect to Respond to Webhook')
|
||||
switch = by_name['route-dispatch'].get('parameters', {})
|
||||
values = switch.get('rules', {}).get('values', [])
|
||||
names = {v.get('outputKey') for v in values if isinstance(v, dict)}
|
||||
if 'notify' not in names:
|
||||
fail('route-dispatch must route notify')
|
||||
|
||||
telegram = by_name['Send Telegram Notification']
|
||||
if telegram.get('credentials', {}).get('telegramApi', {}).get('name') != 'Telegram Bot (OpenClaw)':
|
||||
fail('Send Telegram Notification must use Telegram Bot (OpenClaw) credential')
|
||||
|
||||
discord = by_name['Send Discord Notification']
|
||||
if discord.get('credentials', {}).get('httpHeaderAuth', {}).get('name') != 'Discord Bot Auth':
|
||||
fail('Send Discord Notification must use Discord Bot Auth credential')
|
||||
|
||||
responder = by_name['Respond to Webhook'].get('parameters', {})
|
||||
if responder.get('respondWith') != 'json':
|
||||
fail('Respond to Webhook must respondWith json')
|
||||
if responder.get('responseBody') != '={{$json.response_body}}':
|
||||
fail('Respond to Webhook must use $json.response_body as responseBody')
|
||||
|
||||
sample_paths = [
|
||||
path.parent / 'test-append-log.json',
|
||||
path.parent / 'test-notify.json',
|
||||
]
|
||||
for sample in sample_paths:
|
||||
for sample in (path.parent / 'test-append-log.json', path.parent / 'test-notify.json'):
|
||||
sample_data = load_json(sample)
|
||||
if not isinstance(sample_data, dict):
|
||||
fail(f'sample payload must be an object: {sample}')
|
||||
if 'action' not in sample_data or 'args' not in sample_data:
|
||||
if not isinstance(sample_data, dict) or 'action' not in sample_data or 'args' not in sample_data:
|
||||
fail(f'sample payload missing action/args: {sample}')
|
||||
|
||||
print('OK: workflow asset structure looks consistent')
|
||||
print(f'- workflow: {path}')
|
||||
print(f'- nodes: {len(nodes)}')
|
||||
print('- router: code node with append_log + notify + fallback')
|
||||
print('- routes: append_log -> workflow static data, notify -> Telegram + Discord, fallback -> JSON error')
|
||||
print('- samples: test-append-log.json, test-notify.json')
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user