feat(n8n): add get-logs action

This commit is contained in:
zap
2026-03-12 16:45:19 +00:00
parent c1f851b451
commit aa0914f734
6 changed files with 90 additions and 22 deletions

View File

@@ -30,3 +30,12 @@
- Final fix: switched `append_log` to use n8n workflow static data (`$getWorkflowStaticData('global')`) under key `actionLog`, capped to the latest 200 entries.
- Verified persisted state via the n8n API: `staticData.global.actionLog` contains the live test record for request `live-log-003`.
- Conclusion: for small recent operational breadcrumbs, workflow static data is the right sink here; MinIO is better reserved for later archival/rotation/export use cases rather than tiny per-event appends.
- Added action `get_logs` to the live `openclaw-action` workflow and local `n8n-webhook` skill.
- `get_logs` reads from workflow static data key `actionLog`
- default limit `20`, clamped to `1..50`, newest-first
- verified live with request `live-getlogs-001` returning the seed record from `live-log-004`
- Re-verified the three live actions together after the update:
- `append_log` → success
- `get_logs` → success
- `notify` → success
- Refreshed packaged skill artifact again at `/tmp/n8n-skill-dist/n8n-webhook.skill`.

View File

@@ -51,6 +51,7 @@ Call the preferred action-bus route:
```bash
scripts/call-action.sh append_log --args '{"text":"backup complete"}' --request-id auto
scripts/call-action.sh get_logs --args '{"limit":5}' --pretty
```
Call a test webhook while editing a flow:
@@ -98,6 +99,7 @@ This keeps the external surface small while letting n8n route internally.
Use the included workflow asset when you want a ready-made local router for:
- `append_log` → append small records into workflow static data (`actionLog`, latest 200)
- `get_logs` → read the most recent retained records from `actionLog`
- `notify` → send through the current Telegram + Discord notification paths
- normalized JSON success/failure responses
- unknown-action handling

View File

@@ -30,7 +30,7 @@
"parameters": {
"mode": "runOnceForEachItem",
"language": "javaScript",
"jsCode": "const body = $json.body ?? {};\nconst action = body.action ?? '';\nconst args = body.args ?? {};\nconst requestId = body.request_id ?? '';\nconst now = new Date().toISOString();\nconst workflowStaticData = $getWorkflowStaticData('global');\nconst maxLogEntries = 200;\n\nlet route = 'respond';\nlet statusCode = 400;\nlet responseBody = {\n ok: false,\n request_id: requestId,\n error: { code: 'unknown_action', message: 'action is not supported' },\n};\nlet notifyText = '';\n\nif (action === 'append_log') {\n if (typeof args.text === 'string' && args.text.length > 0) {\n statusCode = 200;\n const record = {\n ts: now,\n source: 'openclaw-action',\n request_id: requestId,\n text: args.text,\n meta: typeof args.meta === 'object' && args.meta !== null ? args.meta : undefined,\n };\n const actionLog = Array.isArray(workflowStaticData.actionLog) ? workflowStaticData.actionLog : [];\n actionLog.push(record);\n if (actionLog.length > maxLogEntries) {\n actionLog.splice(0, actionLog.length - maxLogEntries);\n }\n workflowStaticData.actionLog = actionLog;\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'append_log',\n status: 'logged',\n preview: { text: args.text },\n sink: {\n type: 'workflow-static-data',\n key: 'actionLog',\n retained_entries: maxLogEntries,\n },\n },\n };\n } else {\n responseBody = {\n ok: false,\n request_id: requestId,\n error: { code: 'invalid_request', message: 'required args are missing' },\n };\n }\n} else if (action === 'notify') {\n if (typeof args.message === 'string' && args.message.length > 0) {\n route = 'notify';\n statusCode = 200;\n const title = typeof args.title === 'string' ? args.title : '';\n notifyText = title ? `🔔 ${title}\\n${args.message}` : `🔔 ${args.message}`;\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'notify',\n status: 'sent',\n preview: { title, message: args.message },\n targets: ['telegram', 'discord'],\n },\n };\n } else {\n responseBody = {\n ok: false,\n request_id: requestId,\n error: { code: 'invalid_request', message: 'required args are missing' },\n };\n }\n}\n\nreturn {\n json: {\n route,\n status_code: statusCode,\n response_body: responseBody,\n notify_text: notifyText,\n },\n};"
"jsCode": "const body = $json.body ?? {};\nconst action = body.action ?? '';\nconst args = body.args ?? {};\nconst requestId = body.request_id ?? '';\nconst now = new Date().toISOString();\nconst workflowStaticData = $getWorkflowStaticData('global');\nconst maxLogEntries = 200;\n\nlet route = 'respond';\nlet statusCode = 400;\nlet responseBody = {\n ok: false,\n request_id: requestId,\n error: { code: 'unknown_action', message: 'action is not supported' },\n};\nlet notifyText = '';\n\nif (action === 'append_log') {\n if (typeof args.text === 'string' && args.text.length > 0) {\n statusCode = 200;\n const record = {\n ts: now,\n source: 'openclaw-action',\n request_id: requestId,\n text: args.text,\n meta: typeof args.meta === 'object' && args.meta !== null ? args.meta : undefined,\n };\n const actionLog = Array.isArray(workflowStaticData.actionLog) ? workflowStaticData.actionLog : [];\n actionLog.push(record);\n if (actionLog.length > maxLogEntries) {\n actionLog.splice(0, actionLog.length - maxLogEntries);\n }\n workflowStaticData.actionLog = actionLog;\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'append_log',\n status: 'logged',\n preview: { text: args.text },\n sink: {\n type: 'workflow-static-data',\n key: 'actionLog',\n retained_entries: maxLogEntries,\n },\n },\n };\n } else {\n responseBody = {\n ok: false,\n request_id: requestId,\n error: { code: 'invalid_request', message: 'required args are missing' },\n };\n }\n} else if (action === 'get_logs') {\n const actionLog = Array.isArray(workflowStaticData.actionLog) ? workflowStaticData.actionLog : [];\n const rawLimit = Number.isFinite(Number(args.limit)) ? Number(args.limit) : 20;\n const limit = Math.max(1, Math.min(50, Math.trunc(rawLimit) || 20));\n const entries = actionLog.slice(-limit).reverse();\n statusCode = 200;\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'get_logs',\n status: 'ok',\n count: entries.length,\n total_retained: actionLog.length,\n retained_entries: maxLogEntries,\n entries,\n },\n };\n} else if (action === 'notify') {\n if (typeof args.message === 'string' && args.message.length > 0) {\n route = 'notify';\n statusCode = 200;\n const title = typeof args.title === 'string' ? args.title : '';\n notifyText = title ? `🔔 ${title}\\n${args.message}` : `🔔 ${args.message}`;\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'notify',\n status: 'sent',\n preview: { title, message: args.message },\n targets: ['telegram', 'discord'],\n },\n };\n } else {\n responseBody = {\n ok: false,\n request_id: requestId,\n error: { code: 'invalid_request', message: 'required args are missing' },\n };\n }\n}\n\nreturn {\n json: {\n route,\n status_code: statusCode,\n response_body: responseBody,\n notify_text: notifyText,\n },\n};"
}
},
{
@@ -211,8 +211,8 @@
"staticData": null,
"meta": {
"templateCredsSetupCompleted": false,
"note": "After import, set Webhook authentication to Header Auth and bind a local credential using x-openclaw-secret. This asset ships real append_log persistence via workflow static data plus Telegram/Discord notify fan-out."
"note": "After import, set Webhook authentication to Header Auth and bind a local credential using x-openclaw-secret. This asset ships append_log + get_logs via workflow static data plus Telegram/Discord notify fan-out."
},
"active": false,
"versionId": "openclaw-action-v5"
"versionId": "openclaw-action-v6"
}

View File

@@ -10,8 +10,9 @@ It implements a real local OpenClaw → n8n router.
- accepts `POST /webhook/openclaw-action`
- normalizes incoming JSON into an action contract
- supports two live actions:
- supports three live actions:
- `append_log`
- `get_logs`
- `notify`
- returns normalized JSON responses
- returns `400` for unknown actions
@@ -32,6 +33,24 @@ Example stored record:
{"ts":"2026-03-12T07:00:00Z","source":"openclaw-action","request_id":"abc","text":"backup complete"}
```
### `get_logs`
- reads from workflow static data key:
- `actionLog`
- returns newest-first
- default `limit` is `20`
- clamps `limit` to `1..50`
### `notify`
- sends a Telegram message using credential:
- `Telegram Bot (OpenClaw)`
- sends a Discord message using credential:
- `Discord Bot Auth`
- current targets mirror the already-working reminder workflow
## Why workflow static data for logs
Why this first:
- built-in, no extra credentials
- persists without guessing writable filesystem paths
@@ -43,14 +62,6 @@ When to use MinIO later:
- large/batched exports
- sharing logs outside n8n
### `notify`
- sends a Telegram message using credential:
- `Telegram Bot (OpenClaw)`
- sends a Discord message using credential:
- `Discord Bot Auth`
- current targets mirror the already-working reminder workflow
## Intentional security choice
The exported workflow leaves the Webhook node auth unset in the JSON file.
@@ -92,6 +103,7 @@ After import, set this manually in n8n:
```bash
export N8N_WEBHOOK_SECRET='YOUR_SECRET_HERE'
scripts/call-action.sh append_log --args '{"text":"backup complete"}' --pretty
scripts/call-action.sh get_logs --args '{"limit":5}' --pretty
scripts/call-action.sh notify --args '{"title":"Workflow finished","message":"n8n router test"}' --pretty
```
@@ -118,6 +130,30 @@ scripts/call-action.sh notify --args '{"title":"Workflow finished","message":"n8
}
```
### get_logs
```json
{
"ok": true,
"request_id": "",
"result": {
"action": "get_logs",
"status": "ok",
"count": 1,
"total_retained": 1,
"retained_entries": 200,
"entries": [
{
"ts": "2026-03-12T08:42:37.615Z",
"source": "openclaw-action",
"request_id": "live-log-003",
"text": "n8n append_log static-data verification"
}
]
}
}
```
### notify
```json

View File

@@ -74,6 +74,28 @@ Current sink:
- key: `actionLog`
- retained entries: `200`
### `get_logs`
Request:
```json
{
"action": "get_logs",
"args": {
"limit": 10
}
}
```
Purpose:
- return the most recent retained log records from workflow static data
Behavior:
- default limit: `20`
- min limit: `1`
- max limit: `50`
- entries are returned newest-first
Success shape:
```json
@@ -81,13 +103,12 @@ Success shape:
"ok": true,
"request_id": "optional-uuid",
"result": {
"action": "append_log",
"status": "logged",
"sink": {
"type": "workflow-static-data",
"key": "actionLog",
"retained_entries": 200
}
"action": "get_logs",
"status": "ok",
"count": 2,
"total_retained": 7,
"retained_entries": 200,
"entries": []
}
}
```
@@ -140,5 +161,5 @@ Success shape:
- Use lowercase kebab-case for webhook paths.
- Use lowercase snake_case or kebab-case consistently for action names; prefer snake_case for JSON actions if using switch/router logic.
- Keep names explicit: `openclaw-ping`, `openclaw-action`, `append_log`, `notify`.
- Keep names explicit: `openclaw-ping`, `openclaw-action`, `append_log`, `get_logs`, `notify`.
- Avoid generic names like `run`, `task`, or `webhook1`.

View File

@@ -72,7 +72,7 @@ def main():
router = by_name['route-action'].get('parameters', {})
js_code = router.get('jsCode', '')
for snippet in ('append_log', 'notify', 'unknown_action', 'invalid_request', '$getWorkflowStaticData', 'actionLog', 'retained_entries', 'notify_text'):
for snippet in ('append_log', 'get_logs', 'notify', 'unknown_action', 'invalid_request', '$getWorkflowStaticData', 'actionLog', 'retained_entries', 'notify_text', 'entries.length', 'Math.min(50'):
if snippet not in js_code:
fail(f'route-action jsCode missing expected snippet: {snippet!r}')
@@ -102,7 +102,7 @@ def main():
print('OK: workflow asset structure looks consistent')
print(f'- workflow: {path}')
print(f'- nodes: {len(nodes)}')
print('- routes: append_log -> workflow static data, notify -> Telegram + Discord, fallback -> JSON error')
print('- routes: append_log + get_logs via workflow static data, notify via Telegram + Discord, fallback -> JSON error')
print('- samples: test-append-log.json, test-notify.json')