mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-24 07:01:49 +00:00
refactor: move channel config metadata into plugin-owned manifests
This commit is contained in:
@@ -8773,13 +8773,8 @@
|
||||
],
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"auth",
|
||||
"channels",
|
||||
"network",
|
||||
"security"
|
||||
],
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -9325,13 +9320,8 @@
|
||||
],
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"auth",
|
||||
"channels",
|
||||
"network",
|
||||
"security"
|
||||
],
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -11140,13 +11130,8 @@
|
||||
],
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"auth",
|
||||
"channels",
|
||||
"network",
|
||||
"security"
|
||||
],
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -11408,13 +11393,8 @@
|
||||
],
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"auth",
|
||||
"channels",
|
||||
"network",
|
||||
"security"
|
||||
],
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -11783,14 +11763,8 @@
|
||||
],
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"auth",
|
||||
"channels",
|
||||
"media",
|
||||
"network",
|
||||
"security"
|
||||
],
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -14450,14 +14424,8 @@
|
||||
],
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"auth",
|
||||
"channels",
|
||||
"media",
|
||||
"network",
|
||||
"security"
|
||||
],
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -17055,12 +17023,8 @@
|
||||
],
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"channels",
|
||||
"network",
|
||||
"security"
|
||||
],
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -17118,12 +17082,8 @@
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"channels",
|
||||
"network",
|
||||
"security"
|
||||
],
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -17736,12 +17696,8 @@
|
||||
],
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"channels",
|
||||
"network",
|
||||
"security"
|
||||
],
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -17799,12 +17755,8 @@
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"channels",
|
||||
"network",
|
||||
"security"
|
||||
],
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -19878,13 +19830,8 @@
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"auth",
|
||||
"channels",
|
||||
"network",
|
||||
"security"
|
||||
],
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
@@ -19933,13 +19880,8 @@
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"auth",
|
||||
"channels",
|
||||
"network",
|
||||
"security"
|
||||
],
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
@@ -20700,13 +20642,8 @@
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"auth",
|
||||
"channels",
|
||||
"network",
|
||||
"security"
|
||||
],
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
@@ -23611,13 +23548,8 @@
|
||||
],
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"auth",
|
||||
"channels",
|
||||
"network",
|
||||
"security"
|
||||
],
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -27753,13 +27685,8 @@
|
||||
],
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"auth",
|
||||
"channels",
|
||||
"network",
|
||||
"security"
|
||||
],
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -27851,13 +27778,8 @@
|
||||
],
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"auth",
|
||||
"channels",
|
||||
"network",
|
||||
"security"
|
||||
],
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -28706,13 +28628,8 @@
|
||||
],
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"auth",
|
||||
"channels",
|
||||
"network",
|
||||
"security"
|
||||
],
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -28902,13 +28819,8 @@
|
||||
],
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"auth",
|
||||
"channels",
|
||||
"network",
|
||||
"security"
|
||||
],
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -30116,13 +30028,8 @@
|
||||
],
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"auth",
|
||||
"channels",
|
||||
"network",
|
||||
"security"
|
||||
],
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -30712,13 +30619,8 @@
|
||||
],
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"auth",
|
||||
"channels",
|
||||
"network",
|
||||
"security"
|
||||
],
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -32417,13 +32319,8 @@
|
||||
],
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"auth",
|
||||
"channels",
|
||||
"network",
|
||||
"security"
|
||||
],
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -34567,13 +34464,8 @@
|
||||
],
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"auth",
|
||||
"channels",
|
||||
"network",
|
||||
"security"
|
||||
],
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
|
||||
@@ -767,7 +767,7 @@
|
||||
{"recordType":"path","path":"channels.bluebubbles.accounts.*.mediaLocalRoots.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.bluebubbles.accounts.*.mediaMaxMb","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.bluebubbles.accounts.*.name","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.bluebubbles.accounts.*.password","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","channels","network","security"],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.bluebubbles.accounts.*.password","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.bluebubbles.accounts.*.password.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.bluebubbles.accounts.*.password.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.bluebubbles.accounts.*.password.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -817,7 +817,7 @@
|
||||
{"recordType":"path","path":"channels.bluebubbles.mediaLocalRoots.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.bluebubbles.mediaMaxMb","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.bluebubbles.name","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.bluebubbles.password","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","channels","network","security"],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.bluebubbles.password","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.bluebubbles.password.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.bluebubbles.password.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.bluebubbles.password.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -988,7 +988,7 @@
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.name","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.pluralkit","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.pluralkit.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.pluralkit.token","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","channels","network","security"],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.pluralkit.token","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.pluralkit.token.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.pluralkit.token.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.pluralkit.token.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -1012,7 +1012,7 @@
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.threadBindings.maxAgeHours","kind":"channel","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.threadBindings.spawnAcpSessions","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.threadBindings.spawnSubagentSessions","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.token","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","channels","network","security"],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.token","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.token.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.token.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.token.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -1047,7 +1047,7 @@
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.providers.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.providers.*.*","kind":"channel","type":["array","boolean","null","number","object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.providers.*.*.*","kind":"channel","type":[],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.providers.*.apiKey","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","channels","media","network","security"],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.providers.*.apiKey","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.providers.*.apiKey.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.providers.*.apiKey.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.voice.tts.providers.*.apiKey.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -1273,7 +1273,7 @@
|
||||
{"recordType":"path","path":"channels.discord.voice.tts.providers.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.discord.voice.tts.providers.*.*","kind":"channel","type":["array","boolean","null","number","object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.discord.voice.tts.providers.*.*.*","kind":"channel","type":[],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.discord.voice.tts.providers.*.apiKey","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","channels","media","network","security"],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.discord.voice.tts.providers.*.apiKey","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.discord.voice.tts.providers.*.apiKey.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.discord.voice.tts.providers.*.apiKey.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.discord.voice.tts.providers.*.apiKey.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -1509,13 +1509,13 @@
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.replyToMode","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.requireMention","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.responsePrefix","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.serviceAccount","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["channels","network","security"],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.serviceAccount","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.serviceAccount.*","kind":"channel","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.serviceAccount.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.serviceAccount.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.serviceAccount.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.serviceAccountFile","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.serviceAccountRef","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":true,"tags":["channels","network","security"],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.serviceAccountRef","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.serviceAccountRef.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.serviceAccountRef.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.serviceAccountRef.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -1572,13 +1572,13 @@
|
||||
{"recordType":"path","path":"channels.googlechat.replyToMode","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.requireMention","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.responsePrefix","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.serviceAccount","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["channels","network","security"],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.googlechat.serviceAccount","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.googlechat.serviceAccount.*","kind":"channel","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.serviceAccount.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.serviceAccount.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.serviceAccount.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.serviceAccountFile","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.serviceAccountRef","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":true,"tags":["channels","network","security"],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.googlechat.serviceAccountRef","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.googlechat.serviceAccountRef.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.serviceAccountRef.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.serviceAccountRef.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -1773,12 +1773,12 @@
|
||||
{"recordType":"path","path":"channels.irc.accounts.*.nick","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.irc.accounts.*.nickserv","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.irc.accounts.*.nickserv.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.irc.accounts.*.nickserv.password","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":true,"tags":["auth","channels","network","security"],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.irc.accounts.*.nickserv.password","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.irc.accounts.*.nickserv.passwordFile","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.irc.accounts.*.nickserv.register","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.irc.accounts.*.nickserv.registerEmail","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.irc.accounts.*.nickserv.service","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.irc.accounts.*.password","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":true,"tags":["auth","channels","network","security"],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.irc.accounts.*.password","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.irc.accounts.*.passwordFile","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.irc.accounts.*.port","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.irc.accounts.*.realname","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -1847,7 +1847,7 @@
|
||||
{"recordType":"path","path":"channels.irc.nickserv.register","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"IRC NickServ Register","help":"If true, send NickServ REGISTER on every connect. Use once for initial registration, then disable.","hasChildren":false}
|
||||
{"recordType":"path","path":"channels.irc.nickserv.registerEmail","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"IRC NickServ Register Email","help":"Email used with NickServ REGISTER (required when register=true).","hasChildren":false}
|
||||
{"recordType":"path","path":"channels.irc.nickserv.service","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"IRC NickServ Service","help":"NickServ service nick (default: NickServ).","hasChildren":false}
|
||||
{"recordType":"path","path":"channels.irc.password","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":true,"tags":["auth","channels","network","security"],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.irc.password","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.irc.passwordFile","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.irc.port","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.irc.realname","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -2110,7 +2110,7 @@
|
||||
{"recordType":"path","path":"channels.msteams.allowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.msteams.allowFrom.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.msteams.appId","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.msteams.appPassword","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","channels","network","security"],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.msteams.appPassword","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.msteams.appPassword.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.msteams.appPassword.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.msteams.appPassword.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -2500,7 +2500,7 @@
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.allowBots","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.allowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.allowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.appToken","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","channels","network","security"],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.appToken","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.appToken.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.appToken.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.appToken.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -2509,7 +2509,7 @@
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.blockStreamingCoalesce.idleMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.blockStreamingCoalesce.maxChars","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.blockStreamingCoalesce.minChars","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.botToken","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","channels","network","security"],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.botToken","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.botToken.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.botToken.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.botToken.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -2588,7 +2588,7 @@
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.replyToModeByChatType.group","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.requireMention","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.responsePrefix","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.signingSecret","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","channels","network","security"],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.signingSecret","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.signingSecret.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.signingSecret.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.signingSecret.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -2605,7 +2605,7 @@
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.thread.inheritParent","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.thread.initialHistoryLimit","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.typingReaction","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.userToken","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","channels","network","security"],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.userToken","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.userToken.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.userToken.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.userToken.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -2713,7 +2713,7 @@
|
||||
{"recordType":"path","path":"channels.slack.replyToModeByChatType.group","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.requireMention","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.responsePrefix","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.signingSecret","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","channels","network","security"],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.slack.signingSecret","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.slack.signingSecret.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.signingSecret.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.signingSecret.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -2764,7 +2764,7 @@
|
||||
{"recordType":"path","path":"channels.telegram.accounts.*.blockStreamingCoalesce.idleMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.accounts.*.blockStreamingCoalesce.maxChars","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.accounts.*.blockStreamingCoalesce.minChars","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.accounts.*.botToken","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","channels","network","security"],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.telegram.accounts.*.botToken","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.telegram.accounts.*.botToken.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.accounts.*.botToken.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.accounts.*.botToken.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -2922,7 +2922,7 @@
|
||||
{"recordType":"path","path":"channels.telegram.accounts.*.webhookHost","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.accounts.*.webhookPath","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.accounts.*.webhookPort","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.accounts.*.webhookSecret","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","channels","network","security"],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.telegram.accounts.*.webhookSecret","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.telegram.accounts.*.webhookSecret.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.accounts.*.webhookSecret.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.accounts.*.webhookSecret.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -3107,7 +3107,7 @@
|
||||
{"recordType":"path","path":"channels.telegram.webhookHost","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.webhookPath","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.webhookPort","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.webhookSecret","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","channels","network","security"],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.telegram.webhookSecret","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.telegram.webhookSecret.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.webhookSecret.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.webhookSecret.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
|
||||
1
extensions/bluebubbles/channel-config-api.ts
Normal file
1
extensions/bluebubbles/channel-config-api.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { BlueBubblesChannelConfigSchema } from "./src/config-schema.js";
|
||||
@@ -4,14 +4,13 @@ import {
|
||||
adaptScopedAccountAccessor,
|
||||
createScopedChannelConfigAdapter,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
|
||||
import {
|
||||
listBlueBubblesAccountIds,
|
||||
type ResolvedBlueBubblesAccount,
|
||||
resolveBlueBubblesAccount,
|
||||
resolveDefaultBlueBubblesAccountId,
|
||||
} from "./accounts.js";
|
||||
import { BlueBubblesConfigSchema } from "./config-schema.js";
|
||||
import { BlueBubblesChannelConfigSchema } from "./config-schema.js";
|
||||
import type { ChannelPlugin } from "./runtime-api.js";
|
||||
import { normalizeBlueBubblesHandle } from "./targets.js";
|
||||
|
||||
@@ -41,7 +40,7 @@ export const bluebubblesCapabilities: ChannelPlugin<ResolvedBlueBubblesAccount>[
|
||||
};
|
||||
|
||||
export const bluebubblesReload = { configPrefixes: ["channels.bluebubbles"] };
|
||||
export const bluebubblesConfigSchema = buildChannelConfigSchema(BlueBubblesConfigSchema);
|
||||
export const bluebubblesConfigSchema = BlueBubblesChannelConfigSchema;
|
||||
|
||||
export const bluebubblesConfigAdapter =
|
||||
createScopedChannelConfigAdapter<ResolvedBlueBubblesAccount>({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
AllowFromListSchema,
|
||||
buildChannelConfigSchema,
|
||||
buildCatchallMultiAccountChannelSchema,
|
||||
DmPolicySchema,
|
||||
GroupPolicySchema,
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
ToolPolicySchema,
|
||||
} from "openclaw/plugin-sdk/channel-config-schema";
|
||||
import { z } from "zod";
|
||||
import { bluebubblesChannelConfigUiHints } from "./config-ui-hints.js";
|
||||
import { buildSecretInputSchema, hasConfiguredSecretInput } from "./secret-input.js";
|
||||
|
||||
const bluebubblesActionSchema = z
|
||||
@@ -71,3 +73,7 @@ export const BlueBubblesConfigSchema = buildCatchallMultiAccountChannelSchema(
|
||||
).extend({
|
||||
actions: bluebubblesActionSchema,
|
||||
});
|
||||
|
||||
export const BlueBubblesChannelConfigSchema = buildChannelConfigSchema(BlueBubblesConfigSchema, {
|
||||
uiHints: bluebubblesChannelConfigUiHints,
|
||||
});
|
||||
|
||||
12
extensions/bluebubbles/src/config-ui-hints.ts
Normal file
12
extensions/bluebubbles/src/config-ui-hints.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { ChannelConfigUiHint } from "openclaw/plugin-sdk/core";
|
||||
|
||||
export const bluebubblesChannelConfigUiHints = {
|
||||
"": {
|
||||
label: "BlueBubbles",
|
||||
help: "BlueBubbles channel provider configuration used for Apple messaging bridge integrations. Keep DM policy aligned with your trusted sender model in shared deployments.",
|
||||
},
|
||||
dmPolicy: {
|
||||
label: "BlueBubbles DM Policy",
|
||||
help: 'Direct message access control ("pairing" recommended). "open" requires channels.bluebubbles.allowFrom=["*"].',
|
||||
},
|
||||
} satisfies Record<string, ChannelConfigUiHint>;
|
||||
1
extensions/discord/channel-config-api.ts
Normal file
1
extensions/discord/channel-config-api.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { DiscordChannelConfigSchema } from "./src/config-schema.js";
|
||||
@@ -1,3 +1,9 @@
|
||||
import { buildChannelConfigSchema, DiscordConfigSchema } from "./runtime-api.js";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
DiscordConfigSchema,
|
||||
} from "openclaw/plugin-sdk/channel-config-schema";
|
||||
import { discordChannelConfigUiHints } from "./config-ui-hints.js";
|
||||
|
||||
export const DiscordChannelConfigSchema = buildChannelConfigSchema(DiscordConfigSchema);
|
||||
export const DiscordChannelConfigSchema = buildChannelConfigSchema(DiscordConfigSchema, {
|
||||
uiHints: discordChannelConfigUiHints,
|
||||
});
|
||||
|
||||
196
extensions/discord/src/config-ui-hints.ts
Normal file
196
extensions/discord/src/config-ui-hints.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import type { ChannelConfigUiHint } from "openclaw/plugin-sdk/core";
|
||||
|
||||
export const discordChannelConfigUiHints = {
|
||||
"": {
|
||||
label: "Discord",
|
||||
help: "Discord channel provider configuration for bot auth, retry policy, streaming, thread bindings, and optional voice capabilities. Keep privileged intents and advanced features disabled unless needed.",
|
||||
},
|
||||
dmPolicy: {
|
||||
label: "Discord DM Policy",
|
||||
help: 'Direct message access control ("pairing" recommended). "open" requires channels.discord.allowFrom=["*"].',
|
||||
},
|
||||
"dm.policy": {
|
||||
label: "Discord DM Policy",
|
||||
help: 'Direct message access control ("pairing" recommended). "open" requires channels.discord.allowFrom=["*"] (legacy: channels.discord.dm.allowFrom).',
|
||||
},
|
||||
configWrites: {
|
||||
label: "Discord Config Writes",
|
||||
help: "Allow Discord to write config in response to channel events/commands (default: true).",
|
||||
},
|
||||
proxy: {
|
||||
label: "Discord Proxy URL",
|
||||
help: "Proxy URL for Discord gateway + API requests (app-id lookup and allowlist resolution). Set per account via channels.discord.accounts.<id>.proxy.",
|
||||
},
|
||||
"commands.native": {
|
||||
label: "Discord Native Commands",
|
||||
help: 'Override native commands for Discord (bool or "auto").',
|
||||
},
|
||||
"commands.nativeSkills": {
|
||||
label: "Discord Native Skill Commands",
|
||||
help: 'Override native skill commands for Discord (bool or "auto").',
|
||||
},
|
||||
streaming: {
|
||||
label: "Discord Streaming Mode",
|
||||
help: 'Unified Discord stream preview mode: "off" | "partial" | "block" | "progress". "progress" maps to "partial" on Discord. Legacy boolean/streamMode keys are auto-mapped.',
|
||||
},
|
||||
streamMode: {
|
||||
label: "Discord Stream Mode (Legacy)",
|
||||
help: "Legacy Discord preview mode alias (off | partial | block); auto-migrated to channels.discord.streaming.",
|
||||
},
|
||||
"draftChunk.minChars": {
|
||||
label: "Discord Draft Chunk Min Chars",
|
||||
help: 'Minimum chars before emitting a Discord stream preview update when channels.discord.streaming="block" (default: 200).',
|
||||
},
|
||||
"draftChunk.maxChars": {
|
||||
label: "Discord Draft Chunk Max Chars",
|
||||
help: 'Target max size for a Discord stream preview chunk when channels.discord.streaming="block" (default: 800; clamped to channels.discord.textChunkLimit).',
|
||||
},
|
||||
"draftChunk.breakPreference": {
|
||||
label: "Discord Draft Chunk Break Preference",
|
||||
help: "Preferred breakpoints for Discord draft chunks (paragraph | newline | sentence). Default: paragraph.",
|
||||
},
|
||||
"retry.attempts": {
|
||||
label: "Discord Retry Attempts",
|
||||
help: "Max retry attempts for outbound Discord API calls (default: 3).",
|
||||
},
|
||||
"retry.minDelayMs": {
|
||||
label: "Discord Retry Min Delay (ms)",
|
||||
help: "Minimum retry delay in ms for Discord outbound calls.",
|
||||
},
|
||||
"retry.maxDelayMs": {
|
||||
label: "Discord Retry Max Delay (ms)",
|
||||
help: "Maximum retry delay cap in ms for Discord outbound calls.",
|
||||
},
|
||||
"retry.jitter": {
|
||||
label: "Discord Retry Jitter",
|
||||
help: "Jitter factor (0-1) applied to Discord retry delays.",
|
||||
},
|
||||
maxLinesPerMessage: {
|
||||
label: "Discord Max Lines Per Message",
|
||||
help: "Soft max line count per Discord message (default: 17).",
|
||||
},
|
||||
"inboundWorker.runTimeoutMs": {
|
||||
label: "Discord Inbound Worker Timeout (ms)",
|
||||
help: "Optional queued Discord inbound worker timeout in ms. This is separate from Carbon listener timeouts; defaults to 1800000 and can be disabled with 0. Set per account via channels.discord.accounts.<id>.inboundWorker.runTimeoutMs.",
|
||||
},
|
||||
"eventQueue.listenerTimeout": {
|
||||
label: "Discord EventQueue Listener Timeout (ms)",
|
||||
help: "Canonical Discord listener timeout control in ms for gateway normalization/enqueue handlers. Default is 120000 in OpenClaw; set per account via channels.discord.accounts.<id>.eventQueue.listenerTimeout.",
|
||||
},
|
||||
"eventQueue.maxQueueSize": {
|
||||
label: "Discord EventQueue Max Queue Size",
|
||||
help: "Optional Discord EventQueue capacity override (max queued events before backpressure). Set per account via channels.discord.accounts.<id>.eventQueue.maxQueueSize.",
|
||||
},
|
||||
"eventQueue.maxConcurrency": {
|
||||
label: "Discord EventQueue Max Concurrency",
|
||||
help: "Optional Discord EventQueue concurrency override (max concurrent handler executions). Set per account via channels.discord.accounts.<id>.eventQueue.maxConcurrency.",
|
||||
},
|
||||
"threadBindings.enabled": {
|
||||
label: "Discord Thread Binding Enabled",
|
||||
help: "Enable Discord thread binding features (/focus, bound-thread routing/delivery, and thread-bound subagent sessions). Overrides session.threadBindings.enabled when set.",
|
||||
},
|
||||
"threadBindings.idleHours": {
|
||||
label: "Discord Thread Binding Idle Timeout (hours)",
|
||||
help: "Inactivity window in hours for Discord thread-bound sessions (/focus and spawned thread sessions). Set 0 to disable idle auto-unfocus (default: 24). Overrides session.threadBindings.idleHours when set.",
|
||||
},
|
||||
"threadBindings.maxAgeHours": {
|
||||
label: "Discord Thread Binding Max Age (hours)",
|
||||
help: "Optional hard max age in hours for Discord thread-bound sessions. Set 0 to disable hard cap (default: 0). Overrides session.threadBindings.maxAgeHours when set.",
|
||||
},
|
||||
"threadBindings.spawnSubagentSessions": {
|
||||
label: "Discord Thread-Bound Subagent Spawn",
|
||||
help: "Allow subagent spawns with thread=true to auto-create and bind Discord threads (default: false; opt-in). Set true to enable thread-bound subagent spawns for this account/channel.",
|
||||
},
|
||||
"threadBindings.spawnAcpSessions": {
|
||||
label: "Discord Thread-Bound ACP Spawn",
|
||||
help: "Allow /acp spawn to auto-create and bind Discord threads for ACP sessions (default: false; opt-in). Set true to enable thread-bound ACP spawns for this account/channel.",
|
||||
},
|
||||
"ui.components.accentColor": {
|
||||
label: "Discord Component Accent Color",
|
||||
help: "Accent color for Discord component containers (hex). Set per account via channels.discord.accounts.<id>.ui.components.accentColor.",
|
||||
},
|
||||
"intents.presence": {
|
||||
label: "Discord Presence Intent",
|
||||
help: "Enable the Guild Presences privileged intent. Must also be enabled in the Discord Developer Portal. Allows tracking user activities (e.g. Spotify). Default: false.",
|
||||
},
|
||||
"intents.guildMembers": {
|
||||
label: "Discord Guild Members Intent",
|
||||
help: "Enable the Guild Members privileged intent. Must also be enabled in the Discord Developer Portal. Default: false.",
|
||||
},
|
||||
"voice.enabled": {
|
||||
label: "Discord Voice Enabled",
|
||||
help: "Enable Discord voice channel conversations (default: true). Omit channels.discord.voice to keep voice support disabled for the account.",
|
||||
},
|
||||
"voice.autoJoin": {
|
||||
label: "Discord Voice Auto-Join",
|
||||
help: "Voice channels to auto-join on startup (list of guildId/channelId entries).",
|
||||
},
|
||||
"voice.daveEncryption": {
|
||||
label: "Discord Voice DAVE Encryption",
|
||||
help: "Toggle DAVE end-to-end encryption for Discord voice joins (default: true in @discordjs/voice; Discord may require this).",
|
||||
},
|
||||
"voice.decryptionFailureTolerance": {
|
||||
label: "Discord Voice Decrypt Failure Tolerance",
|
||||
help: "Consecutive decrypt failures before DAVE attempts session recovery (passed to @discordjs/voice; default: 24).",
|
||||
},
|
||||
"voice.tts": {
|
||||
label: "Discord Voice Text-to-Speech",
|
||||
help: "Optional TTS overrides for Discord voice playback (merged with messages.tts).",
|
||||
},
|
||||
"pluralkit.enabled": {
|
||||
label: "Discord PluralKit Enabled",
|
||||
help: "Resolve PluralKit proxied messages and treat system members as distinct senders.",
|
||||
},
|
||||
"pluralkit.token": {
|
||||
label: "Discord PluralKit Token",
|
||||
help: "Optional PluralKit token for resolving private systems or members.",
|
||||
},
|
||||
activity: {
|
||||
label: "Discord Presence Activity",
|
||||
help: "Discord presence activity text (defaults to custom status).",
|
||||
},
|
||||
status: {
|
||||
label: "Discord Presence Status",
|
||||
help: "Discord presence status (online, dnd, idle, invisible).",
|
||||
},
|
||||
"autoPresence.enabled": {
|
||||
label: "Discord Auto Presence Enabled",
|
||||
help: "Enable automatic Discord bot presence updates based on runtime/model availability signals. When enabled: healthy=>online, degraded/unknown=>idle, exhausted/unavailable=>dnd.",
|
||||
},
|
||||
"autoPresence.intervalMs": {
|
||||
label: "Discord Auto Presence Check Interval (ms)",
|
||||
help: "How often to evaluate Discord auto-presence state in milliseconds (default: 30000).",
|
||||
},
|
||||
"autoPresence.minUpdateIntervalMs": {
|
||||
label: "Discord Auto Presence Min Update Interval (ms)",
|
||||
help: "Minimum time between actual Discord presence update calls in milliseconds (default: 15000). Prevents status spam on noisy state changes.",
|
||||
},
|
||||
"autoPresence.healthyText": {
|
||||
label: "Discord Auto Presence Healthy Text",
|
||||
help: "Optional custom status text while runtime is healthy (online). If omitted, falls back to static channels.discord.activity when set.",
|
||||
},
|
||||
"autoPresence.degradedText": {
|
||||
label: "Discord Auto Presence Degraded Text",
|
||||
help: "Optional custom status text while runtime/model availability is degraded or unknown (idle).",
|
||||
},
|
||||
"autoPresence.exhaustedText": {
|
||||
label: "Discord Auto Presence Exhausted Text",
|
||||
help: "Optional custom status text while runtime detects exhausted/unavailable model quota (dnd). Supports {reason} template placeholder.",
|
||||
},
|
||||
activityType: {
|
||||
label: "Discord Presence Activity Type",
|
||||
help: "Discord presence activity type (0=Playing,1=Streaming,2=Listening,3=Watching,4=Custom,5=Competing).",
|
||||
},
|
||||
activityUrl: {
|
||||
label: "Discord Presence Activity URL",
|
||||
help: "Discord presence streaming URL (required for activityType=1).",
|
||||
},
|
||||
allowBots: {
|
||||
label: "Discord Allow Bot Messages",
|
||||
help: 'Allow bot-authored messages to trigger Discord replies (default: false). Set "mentions" to only accept bot messages that mention the bot.',
|
||||
},
|
||||
token: {
|
||||
label: "Discord Bot Token",
|
||||
help: "Discord bot token used for gateway and REST API authentication for this provider account. Keep this secret out of committed config and rotate immediately after any leak.",
|
||||
},
|
||||
} satisfies Record<string, ChannelConfigUiHint>;
|
||||
@@ -9,10 +9,9 @@ import {
|
||||
resolveDiscordAccount,
|
||||
type ResolvedDiscordAccount,
|
||||
} from "./accounts.js";
|
||||
import { DiscordChannelConfigSchema } from "./config-schema.js";
|
||||
import {
|
||||
createScopedChannelConfigAdapter,
|
||||
buildChannelConfigSchema,
|
||||
DiscordConfigSchema,
|
||||
getChatChannelMeta,
|
||||
type ChannelPlugin,
|
||||
} from "./runtime-api.js";
|
||||
@@ -70,7 +69,7 @@ export function createDiscordPluginBase(params: {
|
||||
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
|
||||
},
|
||||
reload: { configPrefixes: ["channels.discord"] },
|
||||
configSchema: buildChannelConfigSchema(DiscordConfigSchema),
|
||||
configSchema: DiscordChannelConfigSchema,
|
||||
config: {
|
||||
...discordConfigAdapter,
|
||||
isConfigured: (account) => Boolean(account.token?.trim()),
|
||||
|
||||
1
extensions/googlechat/channel-config-api.ts
Normal file
1
extensions/googlechat/channel-config-api.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { GoogleChatChannelConfigSchema } from "./src/config-schema.js";
|
||||
@@ -1,3 +1,6 @@
|
||||
import { buildChannelConfigSchema, GoogleChatConfigSchema } from "../runtime-api.js";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
GoogleChatConfigSchema,
|
||||
} from "openclaw/plugin-sdk/channel-config-schema";
|
||||
|
||||
export const GoogleChatChannelConfigSchema = buildChannelConfigSchema(GoogleChatConfigSchema);
|
||||
|
||||
1
extensions/imessage/channel-config-api.ts
Normal file
1
extensions/imessage/channel-config-api.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { IMessageChannelConfigSchema } from "./src/config-schema.js";
|
||||
@@ -1,3 +1,9 @@
|
||||
import { buildChannelConfigSchema, IMessageConfigSchema } from "../runtime-api.js";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
IMessageConfigSchema,
|
||||
} from "openclaw/plugin-sdk/channel-config-schema";
|
||||
import { iMessageChannelConfigUiHints } from "./config-ui-hints.js";
|
||||
|
||||
export const IMessageChannelConfigSchema = buildChannelConfigSchema(IMessageConfigSchema);
|
||||
export const IMessageChannelConfigSchema = buildChannelConfigSchema(IMessageConfigSchema, {
|
||||
uiHints: iMessageChannelConfigUiHints,
|
||||
});
|
||||
|
||||
20
extensions/imessage/src/config-ui-hints.ts
Normal file
20
extensions/imessage/src/config-ui-hints.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { ChannelConfigUiHint } from "openclaw/plugin-sdk/core";
|
||||
|
||||
export const iMessageChannelConfigUiHints = {
|
||||
"": {
|
||||
label: "iMessage",
|
||||
help: "iMessage channel provider configuration for CLI integration and DM access policy handling. Use explicit CLI paths when runtime environments have non-standard binary locations.",
|
||||
},
|
||||
dmPolicy: {
|
||||
label: "iMessage DM Policy",
|
||||
help: 'Direct message access control ("pairing" recommended). "open" requires channels.imessage.allowFrom=["*"].',
|
||||
},
|
||||
configWrites: {
|
||||
label: "iMessage Config Writes",
|
||||
help: "Allow iMessage to write config in response to channel events/commands (default: true).",
|
||||
},
|
||||
cliPath: {
|
||||
label: "iMessage CLI Path",
|
||||
help: "Filesystem path to the iMessage bridge CLI binary used for send/receive operations. Set explicitly when the binary is not on PATH in service runtime environments.",
|
||||
},
|
||||
} satisfies Record<string, ChannelConfigUiHint>;
|
||||
@@ -6,18 +6,14 @@ import {
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { createRestrictSendersChannelSecurity } from "openclaw/plugin-sdk/channel-policy";
|
||||
import { createChannelPluginBase } from "openclaw/plugin-sdk/core";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
getChatChannelMeta,
|
||||
IMessageConfigSchema,
|
||||
type ChannelPlugin,
|
||||
} from "../runtime-api.js";
|
||||
import { getChatChannelMeta, type ChannelPlugin } from "../runtime-api.js";
|
||||
import {
|
||||
listIMessageAccountIds,
|
||||
resolveDefaultIMessageAccountId,
|
||||
resolveIMessageAccount,
|
||||
type ResolvedIMessageAccount,
|
||||
} from "./accounts.js";
|
||||
import { IMessageChannelConfigSchema } from "./config-schema.js";
|
||||
import { createIMessageSetupWizardProxy } from "./setup-core.js";
|
||||
|
||||
export const IMESSAGE_CHANNEL = "imessage" as const;
|
||||
@@ -83,7 +79,7 @@ export function createIMessagePluginBase(params: {
|
||||
media: true,
|
||||
},
|
||||
reload: { configPrefixes: ["channels.imessage"] },
|
||||
configSchema: buildChannelConfigSchema(IMessageConfigSchema),
|
||||
configSchema: IMessageChannelConfigSchema,
|
||||
config: {
|
||||
...imessageConfigAdapter,
|
||||
isConfigured: (account) => account.configured,
|
||||
|
||||
1
extensions/irc/channel-config-api.ts
Normal file
1
extensions/irc/channel-config-api.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { IrcChannelConfigSchema } from "./src/config-schema.js";
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
resolveIrcAccount,
|
||||
type ResolvedIrcAccount,
|
||||
} from "./accounts.js";
|
||||
import { IrcConfigSchema } from "./config-schema.js";
|
||||
import { IrcChannelConfigSchema } from "./config-schema.js";
|
||||
import { monitorIrcProvider } from "./monitor.js";
|
||||
import {
|
||||
normalizeIrcMessagingTarget,
|
||||
@@ -38,7 +38,6 @@ import { resolveIrcGroupMatch, resolveIrcRequireMention } from "./policy.js";
|
||||
import { probeIrc } from "./probe.js";
|
||||
import {
|
||||
buildBaseChannelStatusSummary,
|
||||
buildChannelConfigSchema,
|
||||
createAccountStatusSink,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
getChatChannelMeta,
|
||||
@@ -169,7 +168,7 @@ export const ircPlugin: ChannelPlugin<ResolvedIrcAccount, IrcProbe> = createChat
|
||||
blockStreaming: true,
|
||||
},
|
||||
reload: { configPrefixes: ["channels.irc"] },
|
||||
configSchema: buildChannelConfigSchema(IrcConfigSchema),
|
||||
configSchema: IrcChannelConfigSchema,
|
||||
config: {
|
||||
...ircConfigAdapter,
|
||||
isConfigured: (account) => account.configured,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { requireChannelOpenAllowFrom } from "openclaw/plugin-sdk/extension-shared";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
BlockStreamingCoalesceSchema,
|
||||
DmConfigSchema,
|
||||
DmPolicySchema,
|
||||
@@ -9,7 +8,9 @@ import {
|
||||
ReplyRuntimeConfigSchemaShape,
|
||||
ToolPolicySchema,
|
||||
requireOpenAllowFrom,
|
||||
} from "./runtime-api.js";
|
||||
} from "openclaw/plugin-sdk/channel-config-schema";
|
||||
import { z } from "zod";
|
||||
import { ircChannelConfigUiHints } from "./config-ui-hints.js";
|
||||
|
||||
const IrcGroupSchema = z
|
||||
.object({
|
||||
@@ -70,12 +71,12 @@ export const IrcAccountSchemaBase = z
|
||||
.strict();
|
||||
|
||||
export const IrcAccountSchema = IrcAccountSchemaBase.superRefine((value, ctx) => {
|
||||
requireChannelOpenAllowFrom({
|
||||
channel: "irc",
|
||||
requireOpenAllowFrom({
|
||||
policy: value.dmPolicy,
|
||||
allowFrom: value.allowFrom,
|
||||
ctx,
|
||||
requireOpenAllowFrom,
|
||||
path: ["allowFrom"],
|
||||
message: 'channels.irc.dmPolicy="open" requires channels.irc.allowFrom to include "*"',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -83,11 +84,15 @@ export const IrcConfigSchema = IrcAccountSchemaBase.extend({
|
||||
accounts: z.record(z.string(), IrcAccountSchema.optional()).optional(),
|
||||
defaultAccount: z.string().optional(),
|
||||
}).superRefine((value, ctx) => {
|
||||
requireChannelOpenAllowFrom({
|
||||
channel: "irc",
|
||||
requireOpenAllowFrom({
|
||||
policy: value.dmPolicy,
|
||||
allowFrom: value.allowFrom,
|
||||
ctx,
|
||||
requireOpenAllowFrom,
|
||||
path: ["allowFrom"],
|
||||
message: 'channels.irc.dmPolicy="open" requires channels.irc.allowFrom to include "*"',
|
||||
});
|
||||
});
|
||||
|
||||
export const IrcChannelConfigSchema = buildChannelConfigSchema(IrcConfigSchema, {
|
||||
uiHints: ircChannelConfigUiHints,
|
||||
});
|
||||
|
||||
40
extensions/irc/src/config-ui-hints.ts
Normal file
40
extensions/irc/src/config-ui-hints.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { ChannelConfigUiHint } from "openclaw/plugin-sdk/core";
|
||||
|
||||
export const ircChannelConfigUiHints = {
|
||||
"": {
|
||||
label: "IRC",
|
||||
help: "IRC channel provider configuration and compatibility settings for classic IRC transport workflows. Use this section when bridging legacy chat infrastructure into OpenClaw.",
|
||||
},
|
||||
dmPolicy: {
|
||||
label: "IRC DM Policy",
|
||||
help: 'Direct message access control ("pairing" recommended). "open" requires channels.irc.allowFrom=["*"].',
|
||||
},
|
||||
"nickserv.enabled": {
|
||||
label: "IRC NickServ Enabled",
|
||||
help: "Enable NickServ identify/register after connect (defaults to enabled when password is configured).",
|
||||
},
|
||||
"nickserv.service": {
|
||||
label: "IRC NickServ Service",
|
||||
help: "NickServ service nick (default: NickServ).",
|
||||
},
|
||||
"nickserv.password": {
|
||||
label: "IRC NickServ Password",
|
||||
help: "NickServ password used for IDENTIFY/REGISTER (sensitive).",
|
||||
},
|
||||
"nickserv.passwordFile": {
|
||||
label: "IRC NickServ Password File",
|
||||
help: "Optional file path containing NickServ password.",
|
||||
},
|
||||
"nickserv.register": {
|
||||
label: "IRC NickServ Register",
|
||||
help: "If true, send NickServ REGISTER on every connect. Use once for initial registration, then disable.",
|
||||
},
|
||||
"nickserv.registerEmail": {
|
||||
label: "IRC NickServ Register Email",
|
||||
help: "Email used with NickServ REGISTER (required when register=true).",
|
||||
},
|
||||
configWrites: {
|
||||
label: "IRC Config Writes",
|
||||
help: "Allow IRC to write config in response to channel events/commands (default: true).",
|
||||
},
|
||||
} satisfies Record<string, ChannelConfigUiHint>;
|
||||
1
extensions/msteams/channel-config-api.ts
Normal file
1
extensions/msteams/channel-config-api.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { MSTeamsChannelConfigSchema } from "./src/config-schema.js";
|
||||
@@ -23,12 +23,11 @@ import { createComputedAccountStatusAdapter } from "openclaw/plugin-sdk/status-h
|
||||
import type { ChannelMessageActionName, ChannelPlugin, OpenClawConfig } from "../runtime-api.js";
|
||||
import {
|
||||
buildProbeChannelStatusSummary,
|
||||
buildChannelConfigSchema,
|
||||
createDefaultChannelRuntimeState,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
MSTeamsConfigSchema,
|
||||
PAIRING_APPROVED_MESSAGE,
|
||||
} from "../runtime-api.js";
|
||||
import { MSTeamsChannelConfigSchema } from "./config-schema.js";
|
||||
import { resolveMSTeamsGroupToolPolicy } from "./policy.js";
|
||||
import type { ProbeMSTeamsResult } from "./probe.js";
|
||||
import {
|
||||
@@ -366,7 +365,7 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount, ProbeMSTeamsRe
|
||||
resolveToolPolicy: resolveMSTeamsGroupToolPolicy,
|
||||
},
|
||||
reload: { configPrefixes: ["channels.msteams"] },
|
||||
configSchema: buildChannelConfigSchema(MSTeamsConfigSchema),
|
||||
configSchema: MSTeamsChannelConfigSchema,
|
||||
config: {
|
||||
...msteamsConfigAdapter,
|
||||
isConfigured: (_account, cfg) => Boolean(resolveMSTeamsCredentials(cfg.channels?.msteams)),
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import { buildChannelConfigSchema, MSTeamsConfigSchema } from "../runtime-api.js";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
MSTeamsConfigSchema,
|
||||
} from "openclaw/plugin-sdk/channel-config-schema";
|
||||
import { msTeamsChannelConfigUiHints } from "./config-ui-hints.js";
|
||||
|
||||
export const MSTeamsChannelConfigSchema = buildChannelConfigSchema(MSTeamsConfigSchema);
|
||||
export const MSTeamsChannelConfigSchema = buildChannelConfigSchema(MSTeamsConfigSchema, {
|
||||
uiHints: msTeamsChannelConfigUiHints,
|
||||
});
|
||||
|
||||
12
extensions/msteams/src/config-ui-hints.ts
Normal file
12
extensions/msteams/src/config-ui-hints.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { ChannelConfigUiHint } from "openclaw/plugin-sdk/core";
|
||||
|
||||
export const msTeamsChannelConfigUiHints = {
|
||||
"": {
|
||||
label: "MS Teams",
|
||||
help: "Microsoft Teams channel provider configuration and provider-specific policy toggles. Use this section to isolate Teams behavior from other enterprise chat providers.",
|
||||
},
|
||||
configWrites: {
|
||||
label: "MS Teams Config Writes",
|
||||
help: "Allow Microsoft Teams to write config in response to channel events/commands (default: true).",
|
||||
},
|
||||
} satisfies Record<string, ChannelConfigUiHint>;
|
||||
1
extensions/signal/channel-config-api.ts
Normal file
1
extensions/signal/channel-config-api.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { SignalChannelConfigSchema } from "./src/config-schema.js";
|
||||
@@ -1,3 +1,9 @@
|
||||
import { buildChannelConfigSchema, SignalConfigSchema } from "./runtime-api.js";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
SignalConfigSchema,
|
||||
} from "openclaw/plugin-sdk/channel-config-schema";
|
||||
import { signalChannelConfigUiHints } from "./config-ui-hints.js";
|
||||
|
||||
export const SignalChannelConfigSchema = buildChannelConfigSchema(SignalConfigSchema);
|
||||
export const SignalChannelConfigSchema = buildChannelConfigSchema(SignalConfigSchema, {
|
||||
uiHints: signalChannelConfigUiHints,
|
||||
});
|
||||
|
||||
20
extensions/signal/src/config-ui-hints.ts
Normal file
20
extensions/signal/src/config-ui-hints.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { ChannelConfigUiHint } from "openclaw/plugin-sdk/core";
|
||||
|
||||
export const signalChannelConfigUiHints = {
|
||||
"": {
|
||||
label: "Signal",
|
||||
help: "Signal channel provider configuration including account identity and DM policy behavior. Keep account mapping explicit so routing remains stable across multi-device setups.",
|
||||
},
|
||||
dmPolicy: {
|
||||
label: "Signal DM Policy",
|
||||
help: 'Direct message access control ("pairing" recommended). "open" requires channels.signal.allowFrom=["*"].',
|
||||
},
|
||||
configWrites: {
|
||||
label: "Signal Config Writes",
|
||||
help: "Allow Signal to write config in response to channel events/commands (default: true).",
|
||||
},
|
||||
account: {
|
||||
label: "Signal Account",
|
||||
help: "Signal account identifier (phone/number handle) used to bind this channel config to a specific Signal identity. Keep this aligned with your linked device/session state.",
|
||||
},
|
||||
} satisfies Record<string, ChannelConfigUiHint>;
|
||||
@@ -11,13 +11,8 @@ import {
|
||||
resolveSignalAccount,
|
||||
type ResolvedSignalAccount,
|
||||
} from "./accounts.js";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
getChatChannelMeta,
|
||||
normalizeE164,
|
||||
SignalConfigSchema,
|
||||
type ChannelPlugin,
|
||||
} from "./runtime-api.js";
|
||||
import { SignalChannelConfigSchema } from "./config-schema.js";
|
||||
import { getChatChannelMeta, normalizeE164, type ChannelPlugin } from "./runtime-api.js";
|
||||
import { createSignalSetupWizardProxy } from "./setup-core.js";
|
||||
|
||||
export const SIGNAL_CHANNEL = "signal" as const;
|
||||
@@ -91,7 +86,7 @@ export function createSignalPluginBase(params: {
|
||||
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
|
||||
},
|
||||
reload: { configPrefixes: ["channels.signal"] },
|
||||
configSchema: buildChannelConfigSchema(SignalConfigSchema),
|
||||
configSchema: SignalChannelConfigSchema,
|
||||
config: {
|
||||
...signalConfigAdapter,
|
||||
isConfigured: (account) => account.configured,
|
||||
|
||||
1
extensions/slack/channel-config-api.ts
Normal file
1
extensions/slack/channel-config-api.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { SlackChannelConfigSchema } from "./src/config-schema.js";
|
||||
@@ -1,3 +1,9 @@
|
||||
import { buildChannelConfigSchema, SlackConfigSchema } from "./runtime-api.js";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
SlackConfigSchema,
|
||||
} from "openclaw/plugin-sdk/channel-config-schema";
|
||||
import { slackChannelConfigUiHints } from "./config-ui-hints.js";
|
||||
|
||||
export const SlackChannelConfigSchema = buildChannelConfigSchema(SlackConfigSchema);
|
||||
export const SlackChannelConfigSchema = buildChannelConfigSchema(SlackConfigSchema, {
|
||||
uiHints: slackChannelConfigUiHints,
|
||||
});
|
||||
|
||||
76
extensions/slack/src/config-ui-hints.ts
Normal file
76
extensions/slack/src/config-ui-hints.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { ChannelConfigUiHint } from "openclaw/plugin-sdk/core";
|
||||
|
||||
export const slackChannelConfigUiHints = {
|
||||
"": {
|
||||
label: "Slack",
|
||||
help: "Slack channel provider configuration for bot/app tokens, streaming behavior, and DM policy controls. Keep token handling and thread behavior explicit to avoid noisy workspace interactions.",
|
||||
},
|
||||
"dm.policy": {
|
||||
label: "Slack DM Policy",
|
||||
help: 'Direct message access control ("pairing" recommended). "open" requires channels.slack.allowFrom=["*"] (legacy: channels.slack.dm.allowFrom).',
|
||||
},
|
||||
dmPolicy: {
|
||||
label: "Slack DM Policy",
|
||||
help: 'Direct message access control ("pairing" recommended). "open" requires channels.slack.allowFrom=["*"].',
|
||||
},
|
||||
configWrites: {
|
||||
label: "Slack Config Writes",
|
||||
help: "Allow Slack to write config in response to channel events/commands (default: true).",
|
||||
},
|
||||
"commands.native": {
|
||||
label: "Slack Native Commands",
|
||||
help: 'Override native commands for Slack (bool or "auto").',
|
||||
},
|
||||
"commands.nativeSkills": {
|
||||
label: "Slack Native Skill Commands",
|
||||
help: 'Override native skill commands for Slack (bool or "auto").',
|
||||
},
|
||||
allowBots: {
|
||||
label: "Slack Allow Bot Messages",
|
||||
help: "Allow bot-authored messages to trigger Slack replies (default: false).",
|
||||
},
|
||||
botToken: {
|
||||
label: "Slack Bot Token",
|
||||
help: "Slack bot token used for standard chat actions in the configured workspace. Keep this credential scoped and rotate if workspace app permissions change.",
|
||||
},
|
||||
appToken: {
|
||||
label: "Slack App Token",
|
||||
help: "Slack app-level token used for Socket Mode connections and event transport when enabled. Use least-privilege app scopes and store this token as a secret.",
|
||||
},
|
||||
userToken: {
|
||||
label: "Slack User Token",
|
||||
help: "Optional Slack user token for workflows requiring user-context API access beyond bot permissions. Use sparingly and audit scopes because this token can carry broader authority.",
|
||||
},
|
||||
userTokenReadOnly: {
|
||||
label: "Slack User Token Read Only",
|
||||
help: "When true, treat configured Slack user token usage as read-only helper behavior where possible. Keep enabled if you only need supplemental reads without user-context writes.",
|
||||
},
|
||||
"capabilities.interactiveReplies": {
|
||||
label: "Slack Interactive Replies",
|
||||
help: "Enable agent-authored Slack interactive reply directives (`[[slack_buttons: ...]]`, `[[slack_select: ...]]`). Default: false.",
|
||||
},
|
||||
streaming: {
|
||||
label: "Slack Streaming Mode",
|
||||
help: 'Unified Slack stream preview mode: "off" | "partial" | "block" | "progress". Legacy boolean/streamMode keys are auto-mapped.',
|
||||
},
|
||||
nativeStreaming: {
|
||||
label: "Slack Native Streaming",
|
||||
help: "Enable native Slack text streaming (chat.startStream/chat.appendStream/chat.stopStream) when channels.slack.streaming is partial (default: true).",
|
||||
},
|
||||
streamMode: {
|
||||
label: "Slack Stream Mode (Legacy)",
|
||||
help: "Legacy Slack preview mode alias (replace | status_final | append); auto-migrated to channels.slack.streaming.",
|
||||
},
|
||||
"thread.historyScope": {
|
||||
label: "Slack Thread History Scope",
|
||||
help: 'Scope for Slack thread history context ("thread" isolates per thread; "channel" reuses channel history).',
|
||||
},
|
||||
"thread.inheritParent": {
|
||||
label: "Slack Thread Parent Inheritance",
|
||||
help: "If true, Slack thread sessions inherit the parent channel transcript (default: false).",
|
||||
},
|
||||
"thread.initialHistoryLimit": {
|
||||
label: "Slack Thread Initial History Limit",
|
||||
help: "Maximum number of existing Slack thread messages to fetch when starting a new thread session (default: 20, set to 0 to disable).",
|
||||
},
|
||||
} satisfies Record<string, ChannelConfigUiHint>;
|
||||
@@ -17,14 +17,9 @@ import {
|
||||
resolveSlackAccount,
|
||||
type ResolvedSlackAccount,
|
||||
} from "./accounts.js";
|
||||
import { SlackChannelConfigSchema } from "./config-schema.js";
|
||||
import { isSlackInteractiveRepliesEnabled } from "./interactive-replies.js";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
getChatChannelMeta,
|
||||
SlackConfigSchema,
|
||||
type ChannelPlugin,
|
||||
type OpenClawConfig,
|
||||
} from "./runtime-api.js";
|
||||
import { getChatChannelMeta, type ChannelPlugin, type OpenClawConfig } from "./runtime-api.js";
|
||||
|
||||
export const SLACK_CHANNEL = "slack" as const;
|
||||
|
||||
@@ -204,7 +199,7 @@ export function createSlackPluginBase(params: {
|
||||
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
|
||||
},
|
||||
reload: { configPrefixes: ["channels.slack"] },
|
||||
configSchema: buildChannelConfigSchema(SlackConfigSchema),
|
||||
configSchema: SlackChannelConfigSchema,
|
||||
config: {
|
||||
...slackConfigAdapter,
|
||||
isConfigured: (account) => isSlackPluginAccountConfigured(account),
|
||||
|
||||
1
extensions/telegram/channel-config-api.ts
Normal file
1
extensions/telegram/channel-config-api.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { TelegramChannelConfigSchema } from "./src/config-schema.js";
|
||||
@@ -1,3 +1,9 @@
|
||||
import { buildChannelConfigSchema, TelegramConfigSchema } from "../runtime-api.js";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
TelegramConfigSchema,
|
||||
} from "openclaw/plugin-sdk/channel-config-schema";
|
||||
import { telegramChannelConfigUiHints } from "./config-ui-hints.js";
|
||||
|
||||
export const TelegramChannelConfigSchema = buildChannelConfigSchema(TelegramConfigSchema);
|
||||
export const TelegramChannelConfigSchema = buildChannelConfigSchema(TelegramConfigSchema, {
|
||||
uiHints: telegramChannelConfigUiHints,
|
||||
});
|
||||
|
||||
128
extensions/telegram/src/config-ui-hints.ts
Normal file
128
extensions/telegram/src/config-ui-hints.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import type { ChannelConfigUiHint } from "openclaw/plugin-sdk/core";
|
||||
|
||||
export const telegramChannelConfigUiHints = {
|
||||
"": {
|
||||
label: "Telegram",
|
||||
help: "Telegram channel provider configuration including auth tokens, retry behavior, and message rendering controls. Use this section to tune bot behavior for Telegram-specific API semantics.",
|
||||
},
|
||||
customCommands: {
|
||||
label: "Telegram Custom Commands",
|
||||
help: "Additional Telegram bot menu commands (merged with native; conflicts ignored).",
|
||||
},
|
||||
botToken: {
|
||||
label: "Telegram Bot Token",
|
||||
help: "Telegram bot token used to authenticate Bot API requests for this account/provider config. Use secret/env substitution and rotate tokens if exposure is suspected.",
|
||||
},
|
||||
dmPolicy: {
|
||||
label: "Telegram DM Policy",
|
||||
help: 'Direct message access control ("pairing" recommended). "open" requires channels.telegram.allowFrom=["*"].',
|
||||
},
|
||||
configWrites: {
|
||||
label: "Telegram Config Writes",
|
||||
help: "Allow Telegram to write config in response to channel events/commands (default: true).",
|
||||
},
|
||||
"commands.native": {
|
||||
label: "Telegram Native Commands",
|
||||
help: 'Override native commands for Telegram (bool or "auto").',
|
||||
},
|
||||
"commands.nativeSkills": {
|
||||
label: "Telegram Native Skill Commands",
|
||||
help: 'Override native skill commands for Telegram (bool or "auto").',
|
||||
},
|
||||
streaming: {
|
||||
label: "Telegram Streaming Mode",
|
||||
help: 'Unified Telegram stream preview mode: "off" | "partial" | "block" | "progress" (default: "partial"). "progress" maps to "partial" on Telegram. Legacy boolean/streamMode keys are auto-mapped.',
|
||||
},
|
||||
"retry.attempts": {
|
||||
label: "Telegram Retry Attempts",
|
||||
help: "Max retry attempts for outbound Telegram API calls (default: 3).",
|
||||
},
|
||||
"retry.minDelayMs": {
|
||||
label: "Telegram Retry Min Delay (ms)",
|
||||
help: "Minimum retry delay in ms for Telegram outbound calls.",
|
||||
},
|
||||
"retry.maxDelayMs": {
|
||||
label: "Telegram Retry Max Delay (ms)",
|
||||
help: "Maximum retry delay cap in ms for Telegram outbound calls.",
|
||||
},
|
||||
"retry.jitter": {
|
||||
label: "Telegram Retry Jitter",
|
||||
help: "Jitter factor (0-1) applied to Telegram retry delays.",
|
||||
},
|
||||
"network.autoSelectFamily": {
|
||||
label: "Telegram autoSelectFamily",
|
||||
help: "Override Node autoSelectFamily for Telegram (true=enable, false=disable).",
|
||||
},
|
||||
timeoutSeconds: {
|
||||
label: "Telegram API Timeout (seconds)",
|
||||
help: "Max seconds before Telegram API requests are aborted (default: 500 per grammY).",
|
||||
},
|
||||
silentErrorReplies: {
|
||||
label: "Telegram Silent Error Replies",
|
||||
help: "When true, Telegram bot replies marked as errors are sent silently (no notification sound). Default: false.",
|
||||
},
|
||||
apiRoot: {
|
||||
label: "Telegram API Root URL",
|
||||
help: "Custom Telegram Bot API root URL. Use for self-hosted Bot API servers (https://github.com/tdlib/telegram-bot-api) or reverse proxies in regions where api.telegram.org is blocked.",
|
||||
},
|
||||
autoTopicLabel: {
|
||||
label: "Telegram Auto Topic Label",
|
||||
help: "Auto-rename DM forum topics on first message using LLM. Default: true. Set to false to disable, or use object form { enabled: true, prompt: '...' } for custom prompt.",
|
||||
},
|
||||
"autoTopicLabel.enabled": {
|
||||
label: "Telegram Auto Topic Label Enabled",
|
||||
help: "Whether auto topic labeling is enabled. Default: true.",
|
||||
},
|
||||
"autoTopicLabel.prompt": {
|
||||
label: "Telegram Auto Topic Label Prompt",
|
||||
help: "Custom prompt for LLM-based topic naming. The user message is appended after the prompt.",
|
||||
},
|
||||
"capabilities.inlineButtons": {
|
||||
label: "Telegram Inline Buttons",
|
||||
help: "Enable Telegram inline button components for supported command and interaction surfaces. Disable if your deployment needs plain-text-only compatibility behavior.",
|
||||
},
|
||||
execApprovals: {
|
||||
label: "Telegram Exec Approvals",
|
||||
help: "Telegram-native exec approval routing and approver authorization. Enable this only when Telegram should act as an explicit exec-approval client for the selected bot account.",
|
||||
},
|
||||
"execApprovals.enabled": {
|
||||
label: "Telegram Exec Approvals Enabled",
|
||||
help: "Enable Telegram exec approvals for this account. When false or unset, Telegram messages/buttons cannot approve exec requests.",
|
||||
},
|
||||
"execApprovals.approvers": {
|
||||
label: "Telegram Exec Approval Approvers",
|
||||
help: "Telegram user IDs allowed to approve exec requests for this bot account. Use numeric Telegram user IDs; prompts are only delivered to these approvers when target includes dm.",
|
||||
},
|
||||
"execApprovals.agentFilter": {
|
||||
label: "Telegram Exec Approval Agent Filter",
|
||||
help: 'Optional allowlist of agent IDs eligible for Telegram exec approvals, for example `["main", "ops-agent"]`. Use this to keep approval prompts scoped to the agents you actually operate from Telegram.',
|
||||
},
|
||||
"execApprovals.sessionFilter": {
|
||||
label: "Telegram Exec Approval Session Filter",
|
||||
help: "Optional session-key filters matched as substring or regex-style patterns before Telegram approval routing is used. Use narrow patterns so Telegram approvals only appear for intended sessions.",
|
||||
},
|
||||
"execApprovals.target": {
|
||||
label: "Telegram Exec Approval Target",
|
||||
help: 'Controls where Telegram approval prompts are sent: "dm" sends to approver DMs (default), "channel" sends to the originating Telegram chat/topic, and "both" sends to both. Channel delivery exposes the command text to the chat, so only use it in trusted groups/topics.',
|
||||
},
|
||||
"threadBindings.enabled": {
|
||||
label: "Telegram Thread Binding Enabled",
|
||||
help: "Enable Telegram conversation binding features (/focus, /unfocus, /agents, and /session idle|max-age). Overrides session.threadBindings.enabled when set.",
|
||||
},
|
||||
"threadBindings.idleHours": {
|
||||
label: "Telegram Thread Binding Idle Timeout (hours)",
|
||||
help: "Inactivity window in hours for Telegram bound sessions. Set 0 to disable idle auto-unfocus (default: 24). Overrides session.threadBindings.idleHours when set.",
|
||||
},
|
||||
"threadBindings.maxAgeHours": {
|
||||
label: "Telegram Thread Binding Max Age (hours)",
|
||||
help: "Optional hard max age in hours for Telegram bound sessions. Set 0 to disable hard cap (default: 0). Overrides session.threadBindings.maxAgeHours when set.",
|
||||
},
|
||||
"threadBindings.spawnSubagentSessions": {
|
||||
label: "Telegram Thread-Bound Subagent Spawn",
|
||||
help: "Allow subagent spawns with thread=true to auto-bind Telegram current conversations when supported.",
|
||||
},
|
||||
"threadBindings.spawnAcpSessions": {
|
||||
label: "Telegram Thread-Bound ACP Spawn",
|
||||
help: "Allow ACP spawns with thread=true to auto-bind Telegram current conversations when supported.",
|
||||
},
|
||||
} satisfies Record<string, ChannelConfigUiHint>;
|
||||
@@ -7,10 +7,8 @@ import {
|
||||
import { createChannelPluginBase } from "openclaw/plugin-sdk/core";
|
||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/routing";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
getChatChannelMeta,
|
||||
normalizeAccountId,
|
||||
TelegramConfigSchema,
|
||||
type ChannelPlugin,
|
||||
type OpenClawConfig,
|
||||
} from "../runtime-api.js";
|
||||
@@ -21,6 +19,7 @@ import {
|
||||
resolveTelegramAccount,
|
||||
type ResolvedTelegramAccount,
|
||||
} from "./accounts.js";
|
||||
import { TelegramChannelConfigSchema } from "./config-schema.js";
|
||||
|
||||
export const TELEGRAM_CHANNEL = "telegram" as const;
|
||||
|
||||
@@ -128,7 +127,7 @@ export function createTelegramPluginBase(params: {
|
||||
blockStreaming: true,
|
||||
},
|
||||
reload: { configPrefixes: ["channels.telegram"] },
|
||||
configSchema: buildChannelConfigSchema(TelegramConfigSchema),
|
||||
configSchema: TelegramChannelConfigSchema,
|
||||
config: {
|
||||
...telegramConfigAdapter,
|
||||
isConfigured: (account, cfg) => {
|
||||
|
||||
1
extensions/whatsapp/channel-config-api.ts
Normal file
1
extensions/whatsapp/channel-config-api.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { WhatsAppChannelConfigSchema } from "./src/config-schema.js";
|
||||
@@ -1,3 +1,9 @@
|
||||
import { buildChannelConfigSchema, WhatsAppConfigSchema } from "./runtime-api.js";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
WhatsAppConfigSchema,
|
||||
} from "openclaw/plugin-sdk/channel-config-schema";
|
||||
import { whatsAppChannelConfigUiHints } from "./config-ui-hints.js";
|
||||
|
||||
export const WhatsAppChannelConfigSchema = buildChannelConfigSchema(WhatsAppConfigSchema);
|
||||
export const WhatsAppChannelConfigSchema = buildChannelConfigSchema(WhatsAppConfigSchema, {
|
||||
uiHints: whatsAppChannelConfigUiHints,
|
||||
});
|
||||
|
||||
24
extensions/whatsapp/src/config-ui-hints.ts
Normal file
24
extensions/whatsapp/src/config-ui-hints.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { ChannelConfigUiHint } from "openclaw/plugin-sdk/core";
|
||||
|
||||
export const whatsAppChannelConfigUiHints = {
|
||||
"": {
|
||||
label: "WhatsApp",
|
||||
help: "WhatsApp channel provider configuration for access policy and message batching behavior. Use this section to tune responsiveness and direct-message routing safety for WhatsApp chats.",
|
||||
},
|
||||
dmPolicy: {
|
||||
label: "WhatsApp DM Policy",
|
||||
help: 'Direct message access control ("pairing" recommended). "open" requires channels.whatsapp.allowFrom=["*"].',
|
||||
},
|
||||
selfChatMode: {
|
||||
label: "WhatsApp Self-Phone Mode",
|
||||
help: "Same-phone setup (bot uses your personal WhatsApp number).",
|
||||
},
|
||||
debounceMs: {
|
||||
label: "WhatsApp Message Debounce (ms)",
|
||||
help: "Debounce window (ms) for batching rapid consecutive messages from the same sender (0 to disable).",
|
||||
},
|
||||
configWrites: {
|
||||
label: "WhatsApp Config Writes",
|
||||
help: "Allow WhatsApp to write config in response to channel events/commands (default: true).",
|
||||
},
|
||||
} satisfies Record<string, ChannelConfigUiHint>;
|
||||
@@ -13,15 +13,14 @@ import {
|
||||
resolveWhatsAppAccount,
|
||||
type ResolvedWhatsAppAccount,
|
||||
} from "./accounts.js";
|
||||
import { WhatsAppChannelConfigSchema } from "./config-schema.js";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
formatWhatsAppConfigAllowFromEntries,
|
||||
getChatChannelMeta,
|
||||
normalizeE164,
|
||||
resolveWhatsAppGroupIntroHint,
|
||||
resolveWhatsAppGroupRequireMention,
|
||||
resolveWhatsAppGroupToolPolicy,
|
||||
WhatsAppConfigSchema,
|
||||
type ChannelPlugin,
|
||||
} from "./runtime-api.js";
|
||||
|
||||
@@ -133,7 +132,7 @@ export function createWhatsAppPluginBase(params: {
|
||||
},
|
||||
reload: { configPrefixes: ["web"], noopPrefixes: ["channels.whatsapp"] },
|
||||
gatewayMethods: ["web.login.start", "web.login.wait"],
|
||||
configSchema: buildChannelConfigSchema(WhatsAppConfigSchema),
|
||||
configSchema: WhatsAppChannelConfigSchema,
|
||||
config: {
|
||||
...whatsappConfigAdapter,
|
||||
isEnabled: (account, cfg) => account.enabled && cfg.web?.enabled !== false,
|
||||
|
||||
@@ -642,14 +642,17 @@
|
||||
"canon:check:json": "node scripts/canon.mjs check --json",
|
||||
"canon:enforce": "node scripts/canon.mjs enforce --json",
|
||||
"canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh",
|
||||
"check": "pnpm check:no-conflict-markers && pnpm check:host-env-policy:swift && pnpm check:base-config-schema && pnpm check:bundled-plugin-metadata && pnpm check:bundled-provider-auth-env-vars && pnpm format:check && pnpm tsgo && pnpm plugin-sdk:check-exports && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:tmp:no-raw-channel-fetch && pnpm lint:agent:ingress-owner && pnpm lint:plugins:no-register-http-handler && pnpm lint:plugins:no-monolithic-plugin-sdk-entry-imports && pnpm lint:plugins:no-extension-src-imports && pnpm lint:plugins:no-extension-test-core-imports && pnpm lint:plugins:no-extension-imports && pnpm lint:plugins:plugin-sdk-subpaths-exported && pnpm lint:extensions:no-src-outside-plugin-sdk && pnpm lint:extensions:no-plugin-sdk-internal && pnpm lint:extensions:no-relative-outside-package && pnpm lint:web-search-provider-boundaries && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope",
|
||||
"check": "pnpm check:no-conflict-markers && pnpm check:host-env-policy:swift && pnpm check:bundled-channel-config-metadata && pnpm check:base-config-schema && pnpm check:bundled-plugin-metadata && pnpm check:bundled-provider-auth-env-vars && pnpm format:check && pnpm tsgo && pnpm plugin-sdk:check-exports && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:tmp:no-raw-channel-fetch && pnpm lint:agent:ingress-owner && pnpm lint:plugins:no-register-http-handler && pnpm lint:plugins:no-monolithic-plugin-sdk-entry-imports && pnpm lint:plugins:no-extension-src-imports && pnpm lint:plugins:no-extension-test-core-imports && pnpm lint:plugins:no-extension-imports && pnpm lint:plugins:plugin-sdk-subpaths-exported && pnpm lint:extensions:no-src-outside-plugin-sdk && pnpm lint:extensions:no-plugin-sdk-internal && pnpm lint:extensions:no-relative-outside-package && pnpm lint:web-search-provider-boundaries && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope",
|
||||
"check:base-config-schema": "node --import tsx scripts/generate-base-config-schema.ts --check",
|
||||
"check:bundled-channel-config-metadata": "node --import tsx scripts/generate-bundled-channel-config-metadata.ts --check",
|
||||
"check:bundled-plugin-metadata": "node scripts/generate-bundled-plugin-metadata.mjs --check",
|
||||
"check:bundled-provider-auth-env-vars": "node scripts/generate-bundled-provider-auth-env-vars.mjs --check",
|
||||
"check:docs": "pnpm format:docs:check && pnpm lint:docs && pnpm docs:check-i18n-glossary && pnpm docs:check-links",
|
||||
"check:host-env-policy:swift": "node scripts/generate-host-env-security-policy-swift.mjs --check",
|
||||
"check:loc": "node --import tsx scripts/check-ts-max-loc.ts --max 500",
|
||||
"check:no-conflict-markers": "node scripts/check-no-conflict-markers.mjs",
|
||||
"config:channels:check": "node --import tsx scripts/generate-bundled-channel-config-metadata.ts --check",
|
||||
"config:channels:gen": "node --import tsx scripts/generate-bundled-channel-config-metadata.ts --write",
|
||||
"config:docs:check": "node --import tsx scripts/generate-config-doc-baseline.ts --check",
|
||||
"config:docs:gen": "node --import tsx scripts/generate-config-doc-baseline.ts --write",
|
||||
"config:schema:check": "node --import tsx scripts/generate-base-config-schema.ts --check",
|
||||
|
||||
212
scripts/generate-bundled-channel-config-metadata.ts
Normal file
212
scripts/generate-bundled-channel-config-metadata.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { loadChannelConfigSurfaceModule } from "./load-channel-config-surface.ts";
|
||||
|
||||
const GENERATED_BY = "scripts/generate-bundled-channel-config-metadata.ts";
|
||||
const DEFAULT_OUTPUT_PATH = "src/config/bundled-channel-config-metadata.generated.ts";
|
||||
|
||||
type BundledPluginSource = {
|
||||
dirName: string;
|
||||
pluginDir: string;
|
||||
manifestPath: string;
|
||||
manifest: {
|
||||
id: string;
|
||||
channels?: unknown;
|
||||
name?: string;
|
||||
description?: string;
|
||||
} & Record<string, unknown>;
|
||||
packageJson?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const { collectBundledPluginSources } = (await import(
|
||||
new URL("./lib/bundled-plugin-source-utils.mjs", import.meta.url).href
|
||||
)) as {
|
||||
collectBundledPluginSources: (params?: {
|
||||
repoRoot?: string;
|
||||
requirePackageJson?: boolean;
|
||||
}) => BundledPluginSource[];
|
||||
};
|
||||
|
||||
const { formatGeneratedModule } = (await import(
|
||||
new URL("./lib/format-generated-module.mjs", import.meta.url).href
|
||||
)) as {
|
||||
formatGeneratedModule: (
|
||||
source: string,
|
||||
options: {
|
||||
repoRoot: string;
|
||||
outputPath: string;
|
||||
errorLabel: string;
|
||||
},
|
||||
) => string;
|
||||
};
|
||||
|
||||
const { writeGeneratedOutput } = (await import(
|
||||
new URL("./lib/generated-output-utils.mjs", import.meta.url).href
|
||||
)) as {
|
||||
writeGeneratedOutput: (params: {
|
||||
repoRoot: string;
|
||||
outputPath: string;
|
||||
next: string;
|
||||
check?: boolean;
|
||||
}) => {
|
||||
changed: boolean;
|
||||
wrote: boolean;
|
||||
outputPath: string;
|
||||
};
|
||||
};
|
||||
|
||||
type BundledChannelConfigMetadata = {
|
||||
pluginId: string;
|
||||
channelId: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
schema: Record<string, unknown>;
|
||||
uiHints?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
function resolveChannelConfigSchemaModulePath(rootDir: string): string | null {
|
||||
const candidates = [
|
||||
path.join(rootDir, "src", "config-schema.ts"),
|
||||
path.join(rootDir, "src", "config-schema.js"),
|
||||
path.join(rootDir, "src", "config-schema.mts"),
|
||||
path.join(rootDir, "src", "config-schema.mjs"),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolvePackageChannelMeta(source: BundledPluginSource) {
|
||||
const openclawMeta =
|
||||
source.packageJson &&
|
||||
typeof source.packageJson === "object" &&
|
||||
!Array.isArray(source.packageJson) &&
|
||||
"openclaw" in source.packageJson
|
||||
? (source.packageJson.openclaw as Record<string, unknown> | undefined)
|
||||
: undefined;
|
||||
const channelMeta =
|
||||
openclawMeta &&
|
||||
typeof openclawMeta.channel === "object" &&
|
||||
openclawMeta.channel &&
|
||||
!Array.isArray(openclawMeta.channel)
|
||||
? (openclawMeta.channel as Record<string, unknown>)
|
||||
: undefined;
|
||||
return channelMeta;
|
||||
}
|
||||
|
||||
function resolveRootLabel(source: BundledPluginSource, channelId: string): string | undefined {
|
||||
const channelMeta = resolvePackageChannelMeta(source);
|
||||
if (channelMeta?.id === channelId && typeof channelMeta.label === "string") {
|
||||
return channelMeta.label.trim();
|
||||
}
|
||||
if (typeof source.manifest?.name === "string" && source.manifest.name.trim()) {
|
||||
return source.manifest.name.trim();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveRootDescription(
|
||||
source: BundledPluginSource,
|
||||
channelId: string,
|
||||
): string | undefined {
|
||||
const channelMeta = resolvePackageChannelMeta(source);
|
||||
if (channelMeta?.id === channelId && typeof channelMeta.blurb === "string") {
|
||||
return channelMeta.blurb.trim();
|
||||
}
|
||||
if (typeof source.manifest?.description === "string" && source.manifest.description.trim()) {
|
||||
return source.manifest.description.trim();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function formatTypeScriptModule(source: string, outputPath: string, repoRoot: string): string {
|
||||
return formatGeneratedModule(source, {
|
||||
repoRoot,
|
||||
outputPath,
|
||||
errorLabel: "bundled channel config metadata",
|
||||
});
|
||||
}
|
||||
|
||||
export async function collectBundledChannelConfigMetadata(params?: { repoRoot?: string }) {
|
||||
const repoRoot = path.resolve(params?.repoRoot ?? process.cwd());
|
||||
const sources = collectBundledPluginSources({ repoRoot, requirePackageJson: true });
|
||||
const entries: BundledChannelConfigMetadata[] = [];
|
||||
|
||||
for (const source of sources) {
|
||||
const channelIds = Array.isArray(source.manifest?.channels)
|
||||
? source.manifest.channels.filter(
|
||||
(entry: unknown): entry is string => typeof entry === "string" && entry.trim().length > 0,
|
||||
)
|
||||
: [];
|
||||
if (channelIds.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const modulePath = resolveChannelConfigSchemaModulePath(source.pluginDir);
|
||||
if (!modulePath) {
|
||||
continue;
|
||||
}
|
||||
const surface = await loadChannelConfigSurfaceModule(modulePath, { repoRoot });
|
||||
if (!surface?.schema) {
|
||||
continue;
|
||||
}
|
||||
for (const channelId of channelIds) {
|
||||
const label = resolveRootLabel(source, channelId);
|
||||
const description = resolveRootDescription(source, channelId);
|
||||
entries.push({
|
||||
pluginId: String(source.manifest.id),
|
||||
channelId,
|
||||
...(label ? { label } : {}),
|
||||
...(description ? { description } : {}),
|
||||
schema: surface.schema,
|
||||
...(Object.keys(surface.uiHints ?? {}).length > 0 ? { uiHints: surface.uiHints } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return entries.toSorted((left, right) => left.channelId.localeCompare(right.channelId));
|
||||
}
|
||||
|
||||
export async function writeBundledChannelConfigMetadataModule(params?: {
|
||||
repoRoot?: string;
|
||||
outputPath?: string;
|
||||
check?: boolean;
|
||||
}) {
|
||||
const repoRoot = path.resolve(params?.repoRoot ?? process.cwd());
|
||||
const outputPath = params?.outputPath ?? DEFAULT_OUTPUT_PATH;
|
||||
const entries = await collectBundledChannelConfigMetadata({ repoRoot });
|
||||
const next = formatTypeScriptModule(
|
||||
`// Auto-generated by ${GENERATED_BY}. Do not edit directly.
|
||||
|
||||
export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = ${JSON.stringify(entries, null, 2)} as const;
|
||||
`,
|
||||
outputPath,
|
||||
repoRoot,
|
||||
);
|
||||
return writeGeneratedOutput({
|
||||
repoRoot,
|
||||
outputPath,
|
||||
next,
|
||||
check: params?.check,
|
||||
});
|
||||
}
|
||||
|
||||
if (import.meta.url === new URL(process.argv[1] ?? "", "file://").href) {
|
||||
const check = process.argv.includes("--check");
|
||||
const result = await writeBundledChannelConfigMetadataModule({ check });
|
||||
if (!result.changed) {
|
||||
process.exitCode = 0;
|
||||
} else if (check) {
|
||||
console.error(
|
||||
`[bundled-channel-config-metadata] stale generated output at ${path.relative(process.cwd(), result.outputPath)}`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(
|
||||
`[bundled-channel-config-metadata] wrote ${path.relative(process.cwd(), result.outputPath)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,18 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { buildChannelConfigSchema } from "../src/channels/plugins/config-schema.js";
|
||||
|
||||
function isBuiltChannelConfigSchema(
|
||||
value: unknown,
|
||||
): value is { schema: Record<string, unknown>; uiHints?: Record<string, unknown> } {
|
||||
type BuiltChannelConfigSurface = {
|
||||
schema: Record<string, unknown>;
|
||||
uiHints?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type JsonSchemaCapableSurface = {
|
||||
toJSONSchema?: (params?: Record<string, unknown>) => unknown;
|
||||
uiHints?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
function isBuiltChannelConfigSchema(value: unknown): value is BuiltChannelConfigSurface {
|
||||
if (!value || typeof value !== "object") {
|
||||
return false;
|
||||
}
|
||||
@@ -13,9 +20,32 @@ function isBuiltChannelConfigSchema(
|
||||
return Boolean(candidate.schema && typeof candidate.schema === "object");
|
||||
}
|
||||
|
||||
function resolveConfigSchemaExport(
|
||||
function buildSchemaSurface(value: unknown): BuiltChannelConfigSurface | null {
|
||||
if (!value || typeof value !== "object") {
|
||||
return null;
|
||||
}
|
||||
const candidate = value as JsonSchemaCapableSurface;
|
||||
if (typeof candidate.toJSONSchema === "function") {
|
||||
return {
|
||||
schema: candidate.toJSONSchema({
|
||||
target: "draft-07",
|
||||
unrepresentable: "any",
|
||||
}) as Record<string, unknown>,
|
||||
...(candidate.uiHints ? { uiHints: candidate.uiHints } : {}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
schema: {
|
||||
type: "object",
|
||||
additionalProperties: true,
|
||||
},
|
||||
...(candidate.uiHints ? { uiHints: candidate.uiHints } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveChannelConfigSurfaceExport(
|
||||
imported: Record<string, unknown>,
|
||||
): { schema: Record<string, unknown>; uiHints?: Record<string, unknown> } | null {
|
||||
): BuiltChannelConfigSurface | null {
|
||||
for (const [name, value] of Object.entries(imported)) {
|
||||
if (name.endsWith("ChannelConfigSchema") && isBuiltChannelConfigSchema(value)) {
|
||||
return value;
|
||||
@@ -29,8 +59,9 @@ function resolveConfigSchemaExport(
|
||||
if (isBuiltChannelConfigSchema(value)) {
|
||||
return value;
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
return buildChannelConfigSchema(value as never);
|
||||
const wrapped = buildSchemaSurface(value);
|
||||
if (wrapped) {
|
||||
return wrapped;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,8 +97,7 @@ function shouldRetryViaIsolatedCopy(error: unknown): boolean {
|
||||
return false;
|
||||
}
|
||||
const code = "code" in error ? error.code : undefined;
|
||||
const message = "message" in error && typeof error.message === "string" ? error.message : "";
|
||||
return code === "ERR_MODULE_NOT_FOUND" && message.includes(`${path.sep}node_modules${path.sep}`);
|
||||
return code === "ERR_MODULE_NOT_FOUND";
|
||||
}
|
||||
|
||||
const SOURCE_FILE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
||||
@@ -185,7 +215,7 @@ export async function loadChannelConfigSurfaceModule(
|
||||
|
||||
try {
|
||||
const imported = (await import(pathToFileURL(modulePath).href)) as Record<string, unknown>;
|
||||
return resolveConfigSchemaExport(imported);
|
||||
return resolveChannelConfigSurfaceExport(imported);
|
||||
} catch (error) {
|
||||
if (!shouldRetryViaIsolatedCopy(error)) {
|
||||
throw error;
|
||||
@@ -196,7 +226,7 @@ export async function loadChannelConfigSurfaceModule(
|
||||
const imported = (await import(
|
||||
`${pathToFileURL(isolatedCopy.copiedModulePath).href}?isolated=${Date.now()}`
|
||||
)) as Record<string, unknown>;
|
||||
return resolveConfigSchemaExport(imported);
|
||||
return resolveChannelConfigSurfaceExport(imported);
|
||||
} finally {
|
||||
isolatedCopy.cleanup();
|
||||
}
|
||||
|
||||
@@ -33,4 +33,16 @@ describe("buildChannelConfigSchema", () => {
|
||||
properties: { enabled: { type: "boolean" } },
|
||||
});
|
||||
});
|
||||
|
||||
it("passes through ui hints and exposes a runtime parser", () => {
|
||||
const result = buildChannelConfigSchema(z.object({ enabled: z.boolean().default(true) }), {
|
||||
uiHints: { enabled: { label: "Enabled" } },
|
||||
});
|
||||
|
||||
expect(result.uiHints).toEqual({ enabled: { label: "Enabled" } });
|
||||
expect(result.runtime?.safeParse({})).toEqual({
|
||||
success: true,
|
||||
data: { enabled: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { z, type ZodTypeAny } from "zod";
|
||||
import { DmPolicySchema } from "../../config/zod-schema.core.js";
|
||||
import type { ChannelConfigSchema } from "./types.plugin.js";
|
||||
import type {
|
||||
ChannelConfigRuntimeIssue,
|
||||
ChannelConfigRuntimeParseResult,
|
||||
ChannelConfigSchema,
|
||||
ChannelConfigUiHint,
|
||||
} from "./types.plugin.js";
|
||||
|
||||
type ZodSchemaWithToJsonSchema = ZodTypeAny & {
|
||||
toJSONSchema?: (params?: Record<string, unknown>) => unknown;
|
||||
@@ -32,7 +37,45 @@ export function buildCatchallMultiAccountChannelSchema<T extends ExtendableZodOb
|
||||
}) as T;
|
||||
}
|
||||
|
||||
export function buildChannelConfigSchema(schema: ZodTypeAny): ChannelConfigSchema {
|
||||
type BuildChannelConfigSchemaOptions = {
|
||||
uiHints?: Record<string, ChannelConfigUiHint>;
|
||||
};
|
||||
|
||||
function cloneRuntimeIssue(issue: unknown): ChannelConfigRuntimeIssue {
|
||||
const record = issue && typeof issue === "object" ? (issue as Record<string, unknown>) : {};
|
||||
const path = Array.isArray(record.path)
|
||||
? record.path.filter((segment): segment is string | number => {
|
||||
const kind = typeof segment;
|
||||
return kind === "string" || kind === "number";
|
||||
})
|
||||
: undefined;
|
||||
return {
|
||||
...record,
|
||||
...(path ? { path } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function safeParseRuntimeSchema(
|
||||
schema: ZodTypeAny,
|
||||
value: unknown,
|
||||
): ChannelConfigRuntimeParseResult {
|
||||
const result = schema.safeParse(value);
|
||||
if (result.success) {
|
||||
return {
|
||||
success: true,
|
||||
data: result.data,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
issues: result.error.issues.map((issue) => cloneRuntimeIssue(issue)),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildChannelConfigSchema(
|
||||
schema: ZodTypeAny,
|
||||
options?: BuildChannelConfigSchemaOptions,
|
||||
): ChannelConfigSchema {
|
||||
const schemaWithJson = schema as ZodSchemaWithToJsonSchema;
|
||||
if (typeof schemaWithJson.toJSONSchema === "function") {
|
||||
return {
|
||||
@@ -40,6 +83,10 @@ export function buildChannelConfigSchema(schema: ZodTypeAny): ChannelConfigSchem
|
||||
target: "draft-07",
|
||||
unrepresentable: "any",
|
||||
}) as Record<string, unknown>,
|
||||
...(options?.uiHints ? { uiHints: options.uiHints } : {}),
|
||||
runtime: {
|
||||
safeParse: (value) => safeParseRuntimeSchema(schema, value),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,5 +97,9 @@ export function buildChannelConfigSchema(schema: ZodTypeAny): ChannelConfigSchem
|
||||
type: "object",
|
||||
additionalProperties: true,
|
||||
},
|
||||
...(options?.uiHints ? { uiHints: options.uiHints } : {}),
|
||||
runtime: {
|
||||
safeParse: (value) => safeParseRuntimeSchema(schema, value),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,10 +44,31 @@ export type ChannelConfigUiHint = {
|
||||
itemTemplate?: unknown;
|
||||
};
|
||||
|
||||
export type ChannelConfigRuntimeIssue = {
|
||||
path?: Array<string | number>;
|
||||
message?: string;
|
||||
code?: string;
|
||||
} & Record<string, unknown>;
|
||||
|
||||
export type ChannelConfigRuntimeParseResult =
|
||||
| {
|
||||
success: true;
|
||||
data: unknown;
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
issues: ChannelConfigRuntimeIssue[];
|
||||
};
|
||||
|
||||
export type ChannelConfigRuntimeSchema = {
|
||||
safeParse: (value: unknown) => ChannelConfigRuntimeParseResult;
|
||||
};
|
||||
|
||||
/** JSON-schema-like config description published by a channel plugin. */
|
||||
export type ChannelConfigSchema = {
|
||||
schema: Record<string, unknown>;
|
||||
uiHints?: Record<string, ChannelConfigUiHint>;
|
||||
runtime?: ChannelConfigRuntimeSchema;
|
||||
};
|
||||
|
||||
/** Full capability contract for a native channel plugin. */
|
||||
|
||||
14727
src/config/bundled-channel-config-metadata.generated.ts
Normal file
14727
src/config/bundled-channel-config-metadata.generated.ts
Normal file
File diff suppressed because it is too large
Load Diff
11
src/config/bundled-channel-config-metadata.test.ts
Normal file
11
src/config/bundled-channel-config-metadata.test.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { collectBundledChannelConfigMetadata } from "../../scripts/generate-bundled-channel-config-metadata.ts";
|
||||
import { BUNDLED_CHANNEL_CONFIG_METADATA } from "./bundled-channel-config-metadata.js";
|
||||
|
||||
describe("bundled channel config metadata", () => {
|
||||
it("matches the generated metadata snapshot", async () => {
|
||||
expect(BUNDLED_CHANNEL_CONFIG_METADATA).toEqual(
|
||||
await collectBundledChannelConfigMetadata({ repoRoot: process.cwd() }),
|
||||
);
|
||||
});
|
||||
});
|
||||
14
src/config/bundled-channel-config-metadata.ts
Normal file
14
src/config/bundled-channel-config-metadata.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA } from "./bundled-channel-config-metadata.generated.js";
|
||||
import type { ConfigUiHint } from "./schema.hints.js";
|
||||
|
||||
export type BundledChannelConfigMetadata = {
|
||||
pluginId: string;
|
||||
channelId: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
schema: Record<string, unknown>;
|
||||
uiHints?: Record<string, ConfigUiHint>;
|
||||
};
|
||||
|
||||
export const BUNDLED_CHANNEL_CONFIG_METADATA =
|
||||
GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA as unknown as readonly BundledChannelConfigMetadata[];
|
||||
40
src/config/bundled-channel-config-runtime.ts
Normal file
40
src/config/bundled-channel-config-runtime.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { BlueBubblesChannelConfigSchema } from "../../extensions/bluebubbles/channel-config-api.js";
|
||||
import { DiscordChannelConfigSchema } from "../../extensions/discord/channel-config-api.js";
|
||||
import { GoogleChatChannelConfigSchema } from "../../extensions/googlechat/channel-config-api.js";
|
||||
import { IMessageChannelConfigSchema } from "../../extensions/imessage/channel-config-api.js";
|
||||
import { IrcChannelConfigSchema } from "../../extensions/irc/channel-config-api.js";
|
||||
import { MSTeamsChannelConfigSchema } from "../../extensions/msteams/channel-config-api.js";
|
||||
import { SignalChannelConfigSchema } from "../../extensions/signal/channel-config-api.js";
|
||||
import { SlackChannelConfigSchema } from "../../extensions/slack/channel-config-api.js";
|
||||
import { TelegramChannelConfigSchema } from "../../extensions/telegram/channel-config-api.js";
|
||||
import { WhatsAppChannelConfigSchema } from "../../extensions/whatsapp/channel-config-api.js";
|
||||
import type { ChannelConfigRuntimeSchema } from "../channels/plugins/types.plugin.js";
|
||||
|
||||
type BundledChannelRuntimeMap = ReadonlyMap<string, ChannelConfigRuntimeSchema>;
|
||||
|
||||
const bundledChannelRuntimeEntries: ReadonlyArray<
|
||||
readonly [string, ChannelConfigRuntimeSchema | undefined]
|
||||
> = [
|
||||
["bluebubbles", BlueBubblesChannelConfigSchema.runtime],
|
||||
["discord", DiscordChannelConfigSchema.runtime],
|
||||
["googlechat", GoogleChatChannelConfigSchema.runtime],
|
||||
["imessage", IMessageChannelConfigSchema.runtime],
|
||||
["irc", IrcChannelConfigSchema.runtime],
|
||||
["msteams", MSTeamsChannelConfigSchema.runtime],
|
||||
["signal", SignalChannelConfigSchema.runtime],
|
||||
["slack", SlackChannelConfigSchema.runtime],
|
||||
["telegram", TelegramChannelConfigSchema.runtime],
|
||||
["whatsapp", WhatsAppChannelConfigSchema.runtime],
|
||||
] as const;
|
||||
|
||||
const bundledChannelRuntimeMap = new Map<string, ChannelConfigRuntimeSchema>();
|
||||
for (const [channelId, runtimeSchema] of bundledChannelRuntimeEntries) {
|
||||
if (!runtimeSchema) {
|
||||
continue;
|
||||
}
|
||||
bundledChannelRuntimeMap.set(channelId, runtimeSchema);
|
||||
}
|
||||
|
||||
export function getBundledChannelRuntimeMap(): BundledChannelRuntimeMap {
|
||||
return bundledChannelRuntimeMap;
|
||||
}
|
||||
88
src/config/channel-config-metadata.ts
Normal file
88
src/config/channel-config-metadata.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import type { PluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import type { PluginOrigin } from "../plugins/types.js";
|
||||
import type { ChannelUiMetadata, PluginUiMetadata } from "./schema.js";
|
||||
|
||||
type ChannelMetadataRecord = ChannelUiMetadata & {
|
||||
originRank: number;
|
||||
};
|
||||
|
||||
const PLUGIN_ORIGIN_RANK: Readonly<Record<PluginOrigin, number>> = {
|
||||
config: 0,
|
||||
workspace: 1,
|
||||
global: 2,
|
||||
bundled: 3,
|
||||
};
|
||||
|
||||
export function collectPluginSchemaMetadata(registry: PluginManifestRegistry): PluginUiMetadata[] {
|
||||
const deduped = new Map<
|
||||
string,
|
||||
PluginUiMetadata & {
|
||||
originRank: number;
|
||||
}
|
||||
>();
|
||||
|
||||
for (const record of registry.plugins) {
|
||||
const current = deduped.get(record.id);
|
||||
const nextRank = PLUGIN_ORIGIN_RANK[record.origin] ?? Number.MAX_SAFE_INTEGER;
|
||||
if (current && current.originRank <= nextRank) {
|
||||
continue;
|
||||
}
|
||||
deduped.set(record.id, {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
description: record.description,
|
||||
configUiHints: record.configUiHints,
|
||||
configSchema: record.configSchema,
|
||||
originRank: nextRank,
|
||||
});
|
||||
}
|
||||
|
||||
return [...deduped.values()]
|
||||
.toSorted((left, right) => left.id.localeCompare(right.id))
|
||||
.map(({ originRank: _originRank, ...record }) => record);
|
||||
}
|
||||
|
||||
export function collectChannelSchemaMetadata(
|
||||
registry: PluginManifestRegistry,
|
||||
): ChannelUiMetadata[] {
|
||||
const byChannelId = new Map<string, ChannelMetadataRecord>();
|
||||
|
||||
for (const record of registry.plugins) {
|
||||
const originRank = PLUGIN_ORIGIN_RANK[record.origin] ?? Number.MAX_SAFE_INTEGER;
|
||||
const rootLabel = record.channelCatalogMeta?.label;
|
||||
const rootDescription = record.channelCatalogMeta?.blurb;
|
||||
|
||||
for (const channelId of record.channels) {
|
||||
const current = byChannelId.get(channelId);
|
||||
if (!current || originRank <= current.originRank) {
|
||||
byChannelId.set(channelId, {
|
||||
id: channelId,
|
||||
label: rootLabel ?? current?.label,
|
||||
description: rootDescription ?? current?.description,
|
||||
configSchema: current?.configSchema,
|
||||
configUiHints: current?.configUiHints,
|
||||
originRank,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const [channelId, channelConfig] of Object.entries(record.channelConfigs ?? {})) {
|
||||
const current = byChannelId.get(channelId);
|
||||
if (current && current.originRank < originRank) {
|
||||
continue;
|
||||
}
|
||||
byChannelId.set(channelId, {
|
||||
id: channelId,
|
||||
label: channelConfig.label ?? rootLabel ?? current?.label,
|
||||
description: channelConfig.description ?? rootDescription ?? current?.description,
|
||||
configSchema: channelConfig.schema,
|
||||
configUiHints: channelConfig.uiHints as ChannelUiMetadata["configUiHints"],
|
||||
originRank,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return [...byChannelId.values()]
|
||||
.toSorted((left, right) => left.id.localeCompare(right.id))
|
||||
.map(({ originRank: _originRank, ...entry }) => entry);
|
||||
}
|
||||
59
src/config/channel-config-surface.ts
Normal file
59
src/config/channel-config-surface.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { buildChannelConfigSchema } from "../channels/plugins/config-schema.js";
|
||||
import type { ChannelConfigSchema } from "../channels/plugins/types.plugin.js";
|
||||
|
||||
export type BuiltChannelConfigSurface = ChannelConfigSchema;
|
||||
|
||||
export function isBuiltChannelConfigSchema(value: unknown): value is BuiltChannelConfigSurface {
|
||||
if (!value || typeof value !== "object") {
|
||||
return false;
|
||||
}
|
||||
const candidate = value as { schema?: unknown };
|
||||
return Boolean(candidate.schema && typeof candidate.schema === "object");
|
||||
}
|
||||
|
||||
export function resolveChannelConfigSurfaceExport(
|
||||
imported: Record<string, unknown>,
|
||||
): BuiltChannelConfigSurface | null {
|
||||
for (const [name, value] of Object.entries(imported)) {
|
||||
if (name.endsWith("ChannelConfigSchema") && isBuiltChannelConfigSchema(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [name, value] of Object.entries(imported)) {
|
||||
if (!name.endsWith("ConfigSchema") || name.endsWith("AccountConfigSchema")) {
|
||||
continue;
|
||||
}
|
||||
if (isBuiltChannelConfigSchema(value)) {
|
||||
return value;
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
return buildChannelConfigSchema(value as never);
|
||||
}
|
||||
}
|
||||
|
||||
for (const value of Object.values(imported)) {
|
||||
if (isBuiltChannelConfigSchema(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveChannelConfigSchemaModulePath(rootDir: string): string | null {
|
||||
const candidates = [
|
||||
path.join(rootDir, "src", "config-schema.ts"),
|
||||
path.join(rootDir, "src", "config-schema.js"),
|
||||
path.join(rootDir, "src", "config-schema.mts"),
|
||||
path.join(rootDir, "src", "config-schema.mjs"),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -3,8 +3,14 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js";
|
||||
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import {
|
||||
collectChannelSchemaMetadata,
|
||||
collectPluginSchemaMetadata,
|
||||
} from "./channel-config-metadata.js";
|
||||
import { FIELD_HELP } from "./schema.help.js";
|
||||
import type { ConfigSchemaResponse } from "./schema.js";
|
||||
import { buildConfigSchema } from "./schema.js";
|
||||
import { schemaHasChildren } from "./schema.shared.js";
|
||||
|
||||
type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
|
||||
@@ -25,21 +31,6 @@ type JsonSchemaObject = JsonSchemaNode & {
|
||||
oneOf?: JsonSchemaObject[];
|
||||
};
|
||||
|
||||
type ChannelSurfaceMetadata = {
|
||||
id: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
configSchema?: Record<string, unknown>;
|
||||
configUiHints?: ConfigSchemaResponse["uiHints"];
|
||||
};
|
||||
|
||||
function compareChannelSurfaceMetadata(
|
||||
left: ChannelSurfaceMetadata,
|
||||
right: ChannelSurfaceMetadata,
|
||||
): number {
|
||||
return left.id.localeCompare(right.id);
|
||||
}
|
||||
|
||||
export type ConfigDocBaselineKind = "core" | "channel" | "plugin";
|
||||
|
||||
export type ConfigDocBaselineEntry = {
|
||||
@@ -337,28 +328,7 @@ function resolveEntryKind(configPath: string): ConfigDocBaselineKind {
|
||||
return "core";
|
||||
}
|
||||
|
||||
function resolveFirstExistingPath(candidates: string[]): string | null {
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
fsSync.accessSync(candidate);
|
||||
return candidate;
|
||||
} catch {
|
||||
// Keep scanning for other source file variants.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function loadBundledConfigSchemaResponse(): Promise<ConfigSchemaResponse> {
|
||||
const [
|
||||
{ listChannelPluginCatalogEntries },
|
||||
{ loadPluginManifestRegistry },
|
||||
{ buildConfigSchema },
|
||||
] = await Promise.all([
|
||||
import("../channels/plugins/catalog.js"),
|
||||
import("../plugins/manifest-registry.js"),
|
||||
import("./schema.js"),
|
||||
]);
|
||||
const repoRoot = resolveRepoRoot();
|
||||
const env = {
|
||||
...process.env,
|
||||
@@ -372,135 +342,23 @@ async function loadBundledConfigSchemaResponse(): Promise<ConfigSchemaResponse>
|
||||
env,
|
||||
config: {},
|
||||
});
|
||||
const channelCatalogById = new Map(
|
||||
listChannelPluginCatalogEntries({
|
||||
workspaceDir: repoRoot,
|
||||
env,
|
||||
}).map((entry) => [entry.id, entry.meta] as const),
|
||||
);
|
||||
logConfigDocBaselineDebug(`loaded ${manifestRegistry.plugins.length} bundled plugin manifests`);
|
||||
const bundledChannelPlugins = manifestRegistry.plugins
|
||||
.filter((plugin) => plugin.origin === "bundled" && plugin.channels.length > 0)
|
||||
.toSorted((left, right) => left.id.localeCompare(right.id));
|
||||
const channelPlugins =
|
||||
process.env.OPENCLAW_CONFIG_DOC_BASELINE_DEBUG === "1"
|
||||
? await bundledChannelPlugins.reduce<Promise<ChannelSurfaceMetadata[]>>(
|
||||
async (promise, plugin) => {
|
||||
const loaded = await promise;
|
||||
const catalogMeta = channelCatalogById.get(plugin.id);
|
||||
const label = catalogMeta?.label ?? plugin.name ?? plugin.id;
|
||||
const description = catalogMeta?.blurb ?? plugin.description;
|
||||
loaded.push(
|
||||
(await loadChannelSurfaceMetadata(
|
||||
plugin.rootDir,
|
||||
plugin.id,
|
||||
label,
|
||||
description,
|
||||
repoRoot,
|
||||
)) ?? {
|
||||
id: plugin.id,
|
||||
label,
|
||||
description,
|
||||
configSchema: plugin.configSchema,
|
||||
configUiHints: plugin.configUiHints,
|
||||
},
|
||||
);
|
||||
return loaded;
|
||||
},
|
||||
Promise.resolve([]),
|
||||
)
|
||||
: await Promise.all(
|
||||
bundledChannelPlugins.map(async (plugin) => {
|
||||
const catalogMeta = channelCatalogById.get(plugin.id);
|
||||
const label = catalogMeta?.label ?? plugin.name ?? plugin.id;
|
||||
const description = catalogMeta?.blurb ?? plugin.description;
|
||||
return (
|
||||
(await loadChannelSurfaceMetadata(
|
||||
plugin.rootDir,
|
||||
plugin.id,
|
||||
label,
|
||||
description,
|
||||
repoRoot,
|
||||
)) ?? {
|
||||
id: plugin.id,
|
||||
label,
|
||||
description,
|
||||
configSchema: plugin.configSchema,
|
||||
configUiHints: plugin.configUiHints,
|
||||
}
|
||||
);
|
||||
}),
|
||||
);
|
||||
const bundledRegistry = {
|
||||
...manifestRegistry,
|
||||
plugins: manifestRegistry.plugins.filter((plugin) => plugin.origin === "bundled"),
|
||||
};
|
||||
const channelPlugins = collectChannelSchemaMetadata(bundledRegistry);
|
||||
logConfigDocBaselineDebug(
|
||||
`loaded ${channelPlugins.length} bundled channel entries from channel surfaces`,
|
||||
`loaded ${channelPlugins.length} bundled channel entries from metadata`,
|
||||
);
|
||||
|
||||
return buildConfigSchema({
|
||||
cache: false,
|
||||
plugins: manifestRegistry.plugins
|
||||
.filter((plugin) => plugin.origin === "bundled")
|
||||
.toSorted((left, right) => left.id.localeCompare(right.id))
|
||||
.map((plugin) => ({
|
||||
id: plugin.id,
|
||||
name: plugin.name,
|
||||
description: plugin.description,
|
||||
configUiHints: plugin.configUiHints,
|
||||
configSchema: plugin.configSchema,
|
||||
})),
|
||||
channels: channelPlugins.toSorted(compareChannelSurfaceMetadata).map((entry) => ({
|
||||
id: entry.id,
|
||||
label: entry.label,
|
||||
description: entry.description,
|
||||
configSchema: entry.configSchema,
|
||||
configUiHints: entry.configUiHints,
|
||||
})),
|
||||
plugins: collectPluginSchemaMetadata(bundledRegistry),
|
||||
channels: channelPlugins,
|
||||
});
|
||||
}
|
||||
|
||||
async function loadChannelSurfaceMetadata(
|
||||
rootDir: string,
|
||||
id: string,
|
||||
label: string,
|
||||
description: string | undefined,
|
||||
repoRoot: string,
|
||||
): Promise<ChannelSurfaceMetadata | null> {
|
||||
logConfigDocBaselineDebug(`resolve channel config surface ${rootDir}`);
|
||||
const modulePath = resolveFirstExistingPath([
|
||||
path.join(rootDir, "src", "config-schema.ts"),
|
||||
path.join(rootDir, "src", "config-schema.js"),
|
||||
path.join(rootDir, "src", "config-schema.mts"),
|
||||
path.join(rootDir, "src", "config-schema.mjs"),
|
||||
]);
|
||||
if (!modulePath) {
|
||||
logConfigDocBaselineDebug(`missing channel config schema module ${rootDir}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
logConfigDocBaselineDebug(`import channel config schema ${modulePath}`);
|
||||
try {
|
||||
const { loadChannelConfigSurfaceModule } =
|
||||
await import("../../scripts/load-channel-config-surface.ts");
|
||||
const configSurface = await loadChannelConfigSurfaceModule(modulePath, { repoRoot });
|
||||
if (!configSurface) {
|
||||
logConfigDocBaselineDebug(`channel config schema export missing ${modulePath}`);
|
||||
return null;
|
||||
}
|
||||
logConfigDocBaselineDebug(`completed channel config schema import ${modulePath}`);
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
description,
|
||||
configSchema: configSurface.schema,
|
||||
configUiHints: configSurface.uiHints as ConfigSchemaResponse["uiHints"] | undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
logConfigDocBaselineDebug(
|
||||
`channel config schema import failed for ${modulePath}: ${String(error)}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function collectConfigDocBaselineEntries(
|
||||
schema: JsonSchemaObject,
|
||||
uiHints: ConfigSchemaResponse["uiHints"],
|
||||
|
||||
@@ -3,8 +3,7 @@ import type { ConfigFileSnapshot, OpenClawConfig } from "./types.js";
|
||||
|
||||
const mockLoadConfig = vi.hoisted(() => vi.fn<() => OpenClawConfig>());
|
||||
const mockReadConfigFileSnapshot = vi.hoisted(() => vi.fn<() => Promise<ConfigFileSnapshot>>());
|
||||
const mockLoadOpenClawPlugins = vi.hoisted(() => vi.fn());
|
||||
const mockListChannelPlugins = vi.hoisted(() => vi.fn());
|
||||
const mockLoadPluginManifestRegistry = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./config.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./config.js")>();
|
||||
@@ -15,12 +14,8 @@ vi.mock("./config.js", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../plugins/loader.js", () => ({
|
||||
loadOpenClawPlugins: (...args: unknown[]) => mockLoadOpenClawPlugins(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../channels/plugins/index.js", () => ({
|
||||
listChannelPlugins: (...args: unknown[]) => mockListChannelPlugins(...args),
|
||||
vi.mock("../plugins/manifest-registry.js", () => ({
|
||||
loadPluginManifestRegistry: (...args: unknown[]) => mockLoadPluginManifestRegistry(...args),
|
||||
}));
|
||||
|
||||
function makeSnapshot(params: { valid: boolean; config?: OpenClawConfig }): ConfigFileSnapshot {
|
||||
@@ -38,6 +33,97 @@ function makeSnapshot(params: { valid: boolean; config?: OpenClawConfig }): Conf
|
||||
};
|
||||
}
|
||||
|
||||
function makeManifestRegistry() {
|
||||
return {
|
||||
diagnostics: [],
|
||||
plugins: [
|
||||
{
|
||||
id: "demo",
|
||||
name: "Demo",
|
||||
description: "Demo plugin",
|
||||
origin: "bundled",
|
||||
channels: [],
|
||||
configUiHints: {},
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
mode: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "telegram",
|
||||
name: "Telegram",
|
||||
description: "Telegram plugin",
|
||||
origin: "bundled",
|
||||
channels: ["telegram"],
|
||||
channelCatalogMeta: {
|
||||
id: "telegram",
|
||||
label: "Telegram",
|
||||
blurb: "Telegram channel",
|
||||
},
|
||||
channelConfigs: {
|
||||
telegram: {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
botToken: { type: "string" },
|
||||
},
|
||||
},
|
||||
uiHints: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "slack",
|
||||
name: "Slack",
|
||||
description: "Slack plugin",
|
||||
origin: "bundled",
|
||||
channels: ["slack"],
|
||||
channelCatalogMeta: {
|
||||
id: "slack",
|
||||
label: "Slack",
|
||||
blurb: "Slack channel",
|
||||
},
|
||||
channelConfigs: {
|
||||
slack: {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
botToken: { type: "string" },
|
||||
},
|
||||
},
|
||||
uiHints: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "matrix",
|
||||
name: "Matrix",
|
||||
description: "Matrix plugin",
|
||||
origin: "workspace",
|
||||
channels: ["matrix"],
|
||||
channelCatalogMeta: {
|
||||
id: "matrix",
|
||||
label: "Matrix",
|
||||
blurb: "Matrix channel",
|
||||
},
|
||||
channelConfigs: {
|
||||
matrix: {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
homeserver: { type: "string" },
|
||||
},
|
||||
},
|
||||
uiHints: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async function readSchemaNodes() {
|
||||
const { readBestEffortRuntimeConfigSchema } = await import("./runtime-schema.js");
|
||||
const result = await readBestEffortRuntimeConfigSchema();
|
||||
@@ -55,224 +141,68 @@ describe("readBestEffortRuntimeConfigSchema", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockLoadConfig.mockReturnValue({});
|
||||
mockListChannelPlugins.mockReturnValue([]);
|
||||
mockLoadPluginManifestRegistry.mockReturnValue(makeManifestRegistry());
|
||||
});
|
||||
|
||||
it("uses scoped plugin registry channels for valid configs", async () => {
|
||||
it("merges manifest plugin metadata for valid configs", async () => {
|
||||
mockReadConfigFileSnapshot.mockResolvedValueOnce(
|
||||
makeSnapshot({
|
||||
valid: true,
|
||||
config: { plugins: { entries: { demo: { enabled: true } } } },
|
||||
}),
|
||||
);
|
||||
mockLoadOpenClawPlugins.mockReturnValueOnce({
|
||||
plugins: [
|
||||
{
|
||||
id: "demo",
|
||||
name: "Demo",
|
||||
description: "Demo plugin",
|
||||
configUiHints: {},
|
||||
configJsonSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
mode: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
channels: [
|
||||
{
|
||||
pluginId: "telegram",
|
||||
pluginName: "Telegram",
|
||||
source: "bundled",
|
||||
plugin: {
|
||||
id: "telegram",
|
||||
meta: { label: "Telegram", blurb: "Telegram channel" },
|
||||
configSchema: {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
botToken: { type: "string" },
|
||||
},
|
||||
},
|
||||
uiHints: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
channelSetups: [],
|
||||
});
|
||||
|
||||
const { channelProps, entryProps } = await readSchemaNodes();
|
||||
|
||||
expect(mockLoadOpenClawPlugins).toHaveBeenCalledWith(
|
||||
expect(mockLoadPluginManifestRegistry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: { plugins: { entries: { demo: { enabled: true } } } },
|
||||
activate: false,
|
||||
cache: false,
|
||||
}),
|
||||
);
|
||||
expect(channelProps?.telegram).toBeTruthy();
|
||||
expect(channelProps?.matrix).toBeTruthy();
|
||||
expect(entryProps?.demo).toBeTruthy();
|
||||
});
|
||||
|
||||
it("falls back to channel-only schema when config is invalid", async () => {
|
||||
it("falls back to bundled channel metadata when config is invalid", async () => {
|
||||
mockReadConfigFileSnapshot.mockResolvedValueOnce(makeSnapshot({ valid: false }));
|
||||
mockLoadOpenClawPlugins.mockReturnValueOnce({
|
||||
plugins: [],
|
||||
channels: [
|
||||
{
|
||||
pluginId: "slack",
|
||||
pluginName: "Slack",
|
||||
source: "bundled",
|
||||
plugin: {
|
||||
id: "slack",
|
||||
meta: { label: "Slack", blurb: "Slack channel" },
|
||||
configSchema: {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
botToken: { type: "string" },
|
||||
},
|
||||
},
|
||||
uiHints: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
channelSetups: [
|
||||
{
|
||||
pluginId: "telegram",
|
||||
pluginName: "Telegram",
|
||||
source: "bundled",
|
||||
plugin: {
|
||||
id: "telegram",
|
||||
meta: { label: "Telegram", blurb: "Telegram channel" },
|
||||
configSchema: {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
botToken: { type: "string" },
|
||||
},
|
||||
},
|
||||
uiHints: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { channelProps, entryProps } = await readSchemaNodes();
|
||||
|
||||
expect(mockLoadOpenClawPlugins).toHaveBeenCalledWith(
|
||||
expect(mockLoadPluginManifestRegistry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: { plugins: { enabled: true } },
|
||||
activate: false,
|
||||
cache: false,
|
||||
includeSetupOnlyChannelPlugins: true,
|
||||
}),
|
||||
);
|
||||
expect(channelProps?.telegram).toBeTruthy();
|
||||
expect(channelProps?.slack).toBeTruthy();
|
||||
expect(entryProps?.demo).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not fall back to active registry channels when invalid fallback load throws", async () => {
|
||||
mockReadConfigFileSnapshot.mockResolvedValueOnce(makeSnapshot({ valid: false }));
|
||||
mockLoadOpenClawPlugins.mockImplementationOnce(() => {
|
||||
throw new Error("plugin load failed");
|
||||
});
|
||||
mockListChannelPlugins.mockReturnValueOnce([
|
||||
{
|
||||
id: "telegram",
|
||||
meta: { label: "Telegram", blurb: "Telegram channel" },
|
||||
configSchema: {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
botToken: { type: "string" },
|
||||
},
|
||||
},
|
||||
uiHints: {},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const { readBestEffortRuntimeConfigSchema } = await import("./runtime-schema.js");
|
||||
const result = await readBestEffortRuntimeConfigSchema();
|
||||
const schema = result.schema as { properties?: Record<string, unknown> };
|
||||
const channelsNode = schema.properties?.channels as Record<string, unknown> | undefined;
|
||||
const channelProps = channelsNode?.properties as Record<string, unknown> | undefined;
|
||||
|
||||
expect(channelProps?.telegram).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadGatewayRuntimeConfigSchema", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockLoadConfig.mockReturnValue({ plugins: { entries: { demo: { enabled: true } } } });
|
||||
mockLoadPluginManifestRegistry.mockReturnValue(makeManifestRegistry());
|
||||
});
|
||||
|
||||
it("preserves gateway channel source and loader options", async () => {
|
||||
mockLoadOpenClawPlugins.mockReturnValueOnce({
|
||||
plugins: [
|
||||
{
|
||||
id: "demo",
|
||||
name: "Demo",
|
||||
description: "Demo plugin",
|
||||
configUiHints: {},
|
||||
configJsonSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
mode: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
channels: [
|
||||
{
|
||||
pluginId: "scoped-only",
|
||||
pluginName: "Scoped Only",
|
||||
source: "bundled",
|
||||
plugin: {
|
||||
id: "scoped-only",
|
||||
meta: { label: "Scoped Only" },
|
||||
},
|
||||
},
|
||||
],
|
||||
channelSetups: [],
|
||||
});
|
||||
mockListChannelPlugins.mockReturnValueOnce([
|
||||
{
|
||||
id: "telegram",
|
||||
meta: { label: "Telegram", blurb: "Telegram channel" },
|
||||
configSchema: {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
botToken: { type: "string" },
|
||||
},
|
||||
},
|
||||
uiHints: {},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
it("uses manifest metadata instead of booting plugin runtime", async () => {
|
||||
const { loadGatewayRuntimeConfigSchema } = await import("./runtime-schema.js");
|
||||
const result = loadGatewayRuntimeConfigSchema();
|
||||
const schema = result.schema as { properties?: Record<string, unknown> };
|
||||
const channelsNode = schema.properties?.channels as Record<string, unknown> | undefined;
|
||||
const channelProps = channelsNode?.properties as Record<string, unknown> | undefined;
|
||||
|
||||
expect(mockLoadOpenClawPlugins).toHaveBeenCalledWith(
|
||||
expect(mockLoadPluginManifestRegistry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: { plugins: { entries: { demo: { enabled: true } } } },
|
||||
cache: true,
|
||||
cache: false,
|
||||
}),
|
||||
);
|
||||
expect(mockLoadOpenClawPlugins.mock.calls[0]?.[0]?.activate).toBeUndefined();
|
||||
expect(channelProps?.telegram).toBeTruthy();
|
||||
expect(channelProps?.["scoped-only"]).toBeUndefined();
|
||||
expect(channelProps?.matrix).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,130 +1,38 @@
|
||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { listChannelPlugins, type ChannelPlugin } from "../channels/plugins/index.js";
|
||||
import { loadOpenClawPlugins } from "../plugins/loader.js";
|
||||
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import {
|
||||
collectChannelSchemaMetadata,
|
||||
collectPluginSchemaMetadata,
|
||||
} from "./channel-config-metadata.js";
|
||||
import { loadConfig, readConfigFileSnapshot } from "./config.js";
|
||||
import type { OpenClawConfig } from "./config.js";
|
||||
import { buildConfigSchema, type ChannelUiMetadata, type ConfigSchemaResponse } from "./schema.js";
|
||||
import { buildConfigSchema, type ConfigSchemaResponse } from "./schema.js";
|
||||
|
||||
const silentSchemaLogger = {
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
debug: () => {},
|
||||
};
|
||||
|
||||
function loadPluginSchemaRegistry(
|
||||
config: OpenClawConfig,
|
||||
opts?: {
|
||||
activate?: boolean;
|
||||
cache?: boolean;
|
||||
includeSetupOnlyChannelPlugins?: boolean;
|
||||
},
|
||||
) {
|
||||
function loadManifestRegistry(config: OpenClawConfig, env?: NodeJS.ProcessEnv) {
|
||||
const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
|
||||
return loadOpenClawPlugins({
|
||||
return loadPluginManifestRegistry({
|
||||
config,
|
||||
cache: opts?.cache,
|
||||
activate: opts?.activate,
|
||||
includeSetupOnlyChannelPlugins: opts?.includeSetupOnlyChannelPlugins,
|
||||
cache: false,
|
||||
env,
|
||||
workspaceDir,
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
logger: silentSchemaLogger,
|
||||
});
|
||||
}
|
||||
|
||||
function mapPluginSchemaMetadataFromRegistry(
|
||||
pluginRegistry: ReturnType<typeof loadOpenClawPlugins>,
|
||||
) {
|
||||
return pluginRegistry.plugins.map((plugin) => ({
|
||||
id: plugin.id,
|
||||
name: plugin.name,
|
||||
description: plugin.description,
|
||||
configUiHints: plugin.configUiHints,
|
||||
configSchema: plugin.configJsonSchema,
|
||||
}));
|
||||
}
|
||||
|
||||
function mapChannelSchemaMetadataFromEntries(
|
||||
entries: Array<Pick<ChannelPlugin, "id" | "meta" | "configSchema">>,
|
||||
): ChannelUiMetadata[] {
|
||||
return entries.map((entry) => ({
|
||||
id: entry.id,
|
||||
label: entry.meta.label,
|
||||
description: entry.meta.blurb,
|
||||
configSchema: entry.configSchema?.schema,
|
||||
configUiHints: entry.configSchema?.uiHints,
|
||||
}));
|
||||
}
|
||||
|
||||
function mapActiveChannelSchemaMetadata(): ChannelUiMetadata[] {
|
||||
return mapChannelSchemaMetadataFromEntries(listChannelPlugins());
|
||||
}
|
||||
|
||||
function mapChannelSchemaMetadataFromRegistry(
|
||||
pluginRegistry: ReturnType<typeof loadOpenClawPlugins>,
|
||||
) {
|
||||
const entries = [
|
||||
...pluginRegistry.channelSetups.map((entry) => entry.plugin),
|
||||
...pluginRegistry.channels.map((entry) => entry.plugin),
|
||||
];
|
||||
if (entries.length > 0) {
|
||||
const deduped = new Map<string, Pick<ChannelPlugin, "id" | "meta" | "configSchema">>();
|
||||
for (const entry of entries) {
|
||||
deduped.set(entry.id, entry);
|
||||
}
|
||||
return mapChannelSchemaMetadataFromEntries([...deduped.values()]);
|
||||
}
|
||||
return mapActiveChannelSchemaMetadata();
|
||||
}
|
||||
|
||||
export function loadGatewayRuntimeConfigSchema(): ConfigSchemaResponse {
|
||||
const cfg = loadConfig();
|
||||
const pluginRegistry = loadPluginSchemaRegistry(cfg, { cache: true });
|
||||
const config = loadConfig();
|
||||
const registry = loadManifestRegistry(config);
|
||||
return buildConfigSchema({
|
||||
plugins: mapPluginSchemaMetadataFromRegistry(pluginRegistry),
|
||||
channels: mapActiveChannelSchemaMetadata(),
|
||||
plugins: collectPluginSchemaMetadata(registry),
|
||||
channels: collectChannelSchemaMetadata(registry),
|
||||
});
|
||||
}
|
||||
|
||||
function readFallbackChannelSchemaMetadata(): ChannelUiMetadata[] {
|
||||
try {
|
||||
const pluginRegistry = loadPluginSchemaRegistry(
|
||||
{
|
||||
plugins: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
activate: false,
|
||||
cache: false,
|
||||
includeSetupOnlyChannelPlugins: true,
|
||||
},
|
||||
);
|
||||
return mapChannelSchemaMetadataFromRegistry(pluginRegistry);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function readBestEffortRuntimeConfigSchema(): Promise<ConfigSchemaResponse> {
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
|
||||
if (!snapshot.valid) {
|
||||
return buildConfigSchema({ channels: readFallbackChannelSchemaMetadata() });
|
||||
}
|
||||
|
||||
try {
|
||||
const pluginRegistry = loadPluginSchemaRegistry(snapshot.config, {
|
||||
activate: false,
|
||||
cache: false,
|
||||
});
|
||||
return buildConfigSchema({
|
||||
plugins: mapPluginSchemaMetadataFromRegistry(pluginRegistry),
|
||||
channels: mapChannelSchemaMetadataFromRegistry(pluginRegistry),
|
||||
});
|
||||
} catch {
|
||||
return buildConfigSchema({ channels: readFallbackChannelSchemaMetadata() });
|
||||
}
|
||||
const config = snapshot.valid ? snapshot.config : { plugins: { enabled: true } };
|
||||
const registry = loadManifestRegistry(config);
|
||||
return buildConfigSchema({
|
||||
plugins: snapshot.valid ? collectPluginSchemaMetadata(registry) : [],
|
||||
channels: collectChannelSchemaMetadata(registry),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9546,10 +9546,9 @@ export const GENERATED_BASE_CONFIG_SCHEMA = {
|
||||
additionalProperties: false,
|
||||
},
|
||||
channels: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
additionalProperties: true,
|
||||
required: [],
|
||||
additionalProperties: true,
|
||||
},
|
||||
discovery: {
|
||||
type: "object",
|
||||
@@ -14981,674 +14980,11 @@ export const GENERATED_BASE_CONFIG_SCHEMA = {
|
||||
help: "Enables concise indicator-style heartbeat rendering instead of verbose status text where supported. Use indicator mode for dense dashboards with many active channels.",
|
||||
tags: ["network", "automation", "channels"],
|
||||
},
|
||||
"channels.whatsapp": {
|
||||
label: "WhatsApp",
|
||||
help: "WhatsApp channel provider configuration for access policy and message batching behavior. Use this section to tune responsiveness and direct-message routing safety for WhatsApp chats.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.telegram": {
|
||||
label: "Telegram",
|
||||
help: "Telegram channel provider configuration including auth tokens, retry behavior, and message rendering controls. Use this section to tune bot behavior for Telegram-specific API semantics.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.telegram.customCommands": {
|
||||
label: "Telegram Custom Commands",
|
||||
help: "Additional Telegram bot menu commands (merged with native; conflicts ignored).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.discord": {
|
||||
label: "Discord",
|
||||
help: "Discord channel provider configuration for bot auth, retry policy, streaming, thread bindings, and optional voice capabilities. Keep privileged intents and advanced features disabled unless needed.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.slack": {
|
||||
label: "Slack",
|
||||
help: "Slack channel provider configuration for bot/app tokens, streaming behavior, and DM policy controls. Keep token handling and thread behavior explicit to avoid noisy workspace interactions.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.mattermost": {
|
||||
label: "Mattermost",
|
||||
help: "Mattermost channel provider configuration for bot credentials, base URL, and message trigger modes. Keep mention/trigger rules strict in high-volume team channels.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.signal": {
|
||||
label: "Signal",
|
||||
help: "Signal channel provider configuration including account identity and DM policy behavior. Keep account mapping explicit so routing remains stable across multi-device setups.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.imessage": {
|
||||
label: "iMessage",
|
||||
help: "iMessage channel provider configuration for CLI integration and DM access policy handling. Use explicit CLI paths when runtime environments have non-standard binary locations.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.bluebubbles": {
|
||||
label: "BlueBubbles",
|
||||
help: "BlueBubbles channel provider configuration used for Apple messaging bridge integrations. Keep DM policy aligned with your trusted sender model in shared deployments.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.msteams": {
|
||||
label: "MS Teams",
|
||||
help: "Microsoft Teams channel provider configuration and provider-specific policy toggles. Use this section to isolate Teams behavior from other enterprise chat providers.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.modelByChannel": {
|
||||
label: "Channel Model Overrides",
|
||||
help: "Map provider -> channel id -> model override (values are provider/model or aliases).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.irc": {
|
||||
label: "IRC",
|
||||
help: "IRC channel provider configuration and compatibility settings for classic IRC transport workflows. Use this section when bridging legacy chat infrastructure into OpenClaw.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.irc.dmPolicy": {
|
||||
label: "IRC DM Policy",
|
||||
help: 'Direct message access control ("pairing" recommended). "open" requires channels.irc.allowFrom=["*"].',
|
||||
tags: ["access", "network", "channels"],
|
||||
},
|
||||
"channels.irc.nickserv.enabled": {
|
||||
label: "IRC NickServ Enabled",
|
||||
help: "Enable NickServ identify/register after connect (defaults to enabled when password is configured).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.irc.nickserv.service": {
|
||||
label: "IRC NickServ Service",
|
||||
help: "NickServ service nick (default: NickServ).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.irc.nickserv.password": {
|
||||
label: "IRC NickServ Password",
|
||||
help: "NickServ password used for IDENTIFY/REGISTER (sensitive).",
|
||||
tags: ["security", "auth", "network", "channels"],
|
||||
sensitive: true,
|
||||
},
|
||||
"channels.irc.nickserv.passwordFile": {
|
||||
label: "IRC NickServ Password File",
|
||||
help: "Optional file path containing NickServ password.",
|
||||
tags: ["security", "auth", "network", "storage", "channels"],
|
||||
},
|
||||
"channels.irc.nickserv.register": {
|
||||
label: "IRC NickServ Register",
|
||||
help: "If true, send NickServ REGISTER on every connect. Use once for initial registration, then disable.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.irc.nickserv.registerEmail": {
|
||||
label: "IRC NickServ Register Email",
|
||||
help: "Email used with NickServ REGISTER (required when register=true).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.telegram.botToken": {
|
||||
label: "Telegram Bot Token",
|
||||
help: "Telegram bot token used to authenticate Bot API requests for this account/provider config. Use secret/env substitution and rotate tokens if exposure is suspected.",
|
||||
tags: ["security", "auth", "network", "channels"],
|
||||
sensitive: true,
|
||||
},
|
||||
"channels.telegram.dmPolicy": {
|
||||
label: "Telegram DM Policy",
|
||||
help: 'Direct message access control ("pairing" recommended). "open" requires channels.telegram.allowFrom=["*"].',
|
||||
tags: ["access", "network", "channels"],
|
||||
},
|
||||
"channels.telegram.configWrites": {
|
||||
label: "Telegram Config Writes",
|
||||
help: "Allow Telegram to write config in response to channel events/commands (default: true).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.telegram.commands.native": {
|
||||
label: "Telegram Native Commands",
|
||||
help: 'Override native commands for Telegram (bool or "auto").',
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.telegram.commands.nativeSkills": {
|
||||
label: "Telegram Native Skill Commands",
|
||||
help: 'Override native skill commands for Telegram (bool or "auto").',
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.telegram.streaming": {
|
||||
label: "Telegram Streaming Mode",
|
||||
help: 'Unified Telegram stream preview mode: "off" | "partial" | "block" | "progress" (default: "partial"). "progress" maps to "partial" on Telegram. Legacy boolean/streamMode keys are auto-mapped.',
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.telegram.retry.attempts": {
|
||||
label: "Telegram Retry Attempts",
|
||||
help: "Max retry attempts for outbound Telegram API calls (default: 3).",
|
||||
tags: ["network", "reliability", "channels"],
|
||||
},
|
||||
"channels.telegram.retry.minDelayMs": {
|
||||
label: "Telegram Retry Min Delay (ms)",
|
||||
help: "Minimum retry delay in ms for Telegram outbound calls.",
|
||||
tags: ["network", "reliability", "channels"],
|
||||
},
|
||||
"channels.telegram.retry.maxDelayMs": {
|
||||
label: "Telegram Retry Max Delay (ms)",
|
||||
help: "Maximum retry delay cap in ms for Telegram outbound calls.",
|
||||
tags: ["network", "reliability", "performance", "channels"],
|
||||
},
|
||||
"channels.telegram.retry.jitter": {
|
||||
label: "Telegram Retry Jitter",
|
||||
help: "Jitter factor (0-1) applied to Telegram retry delays.",
|
||||
tags: ["network", "reliability", "channels"],
|
||||
},
|
||||
"channels.telegram.network.autoSelectFamily": {
|
||||
label: "Telegram autoSelectFamily",
|
||||
help: "Override Node autoSelectFamily for Telegram (true=enable, false=disable).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.telegram.timeoutSeconds": {
|
||||
label: "Telegram API Timeout (seconds)",
|
||||
help: "Max seconds before Telegram API requests are aborted (default: 500 per grammY).",
|
||||
tags: ["network", "performance", "channels"],
|
||||
},
|
||||
"channels.telegram.silentErrorReplies": {
|
||||
label: "Telegram Silent Error Replies",
|
||||
help: "When true, Telegram bot replies marked as errors are sent silently (no notification sound). Default: false.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.telegram.apiRoot": {
|
||||
label: "Telegram API Root URL",
|
||||
help: "Custom Telegram Bot API root URL. Use for self-hosted Bot API servers (https://github.com/tdlib/telegram-bot-api) or reverse proxies in regions where api.telegram.org is blocked.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.telegram.autoTopicLabel": {
|
||||
label: "Telegram Auto Topic Label",
|
||||
help: "Auto-rename DM forum topics on first message using LLM. Default: true. Set to false to disable, or use object form { enabled: true, prompt: '...' } for custom prompt.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.telegram.autoTopicLabel.enabled": {
|
||||
label: "Telegram Auto Topic Label Enabled",
|
||||
help: "Whether auto topic labeling is enabled. Default: true.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.telegram.autoTopicLabel.prompt": {
|
||||
label: "Telegram Auto Topic Label Prompt",
|
||||
help: "Custom prompt for LLM-based topic naming. The user message is appended after the prompt.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.telegram.capabilities.inlineButtons": {
|
||||
label: "Telegram Inline Buttons",
|
||||
help: "Enable Telegram inline button components for supported command and interaction surfaces. Disable if your deployment needs plain-text-only compatibility behavior.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.telegram.execApprovals": {
|
||||
label: "Telegram Exec Approvals",
|
||||
help: "Telegram-native exec approval routing and approver authorization. Enable this only when Telegram should act as an explicit exec-approval client for the selected bot account.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.telegram.execApprovals.enabled": {
|
||||
label: "Telegram Exec Approvals Enabled",
|
||||
help: "Enable Telegram exec approvals for this account. When false or unset, Telegram messages/buttons cannot approve exec requests.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.telegram.execApprovals.approvers": {
|
||||
label: "Telegram Exec Approval Approvers",
|
||||
help: "Telegram user IDs allowed to approve exec requests for this bot account. Use numeric Telegram user IDs; prompts are only delivered to these approvers when target includes dm.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.telegram.execApprovals.agentFilter": {
|
||||
label: "Telegram Exec Approval Agent Filter",
|
||||
help: 'Optional allowlist of agent IDs eligible for Telegram exec approvals, for example `["main", "ops-agent"]`. Use this to keep approval prompts scoped to the agents you actually operate from Telegram.',
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.telegram.execApprovals.sessionFilter": {
|
||||
label: "Telegram Exec Approval Session Filter",
|
||||
help: "Optional session-key filters matched as substring or regex-style patterns before Telegram approval routing is used. Use narrow patterns so Telegram approvals only appear for intended sessions.",
|
||||
tags: ["network", "storage", "channels"],
|
||||
},
|
||||
"channels.telegram.execApprovals.target": {
|
||||
label: "Telegram Exec Approval Target",
|
||||
help: 'Controls where Telegram approval prompts are sent: "dm" sends to approver DMs (default), "channel" sends to the originating Telegram chat/topic, and "both" sends to both. Channel delivery exposes the command text to the chat, so only use it in trusted groups/topics.',
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.telegram.threadBindings.enabled": {
|
||||
label: "Telegram Thread Binding Enabled",
|
||||
help: "Enable Telegram conversation binding features (/focus, /unfocus, /agents, and /session idle|max-age). Overrides session.threadBindings.enabled when set.",
|
||||
tags: ["network", "storage", "channels"],
|
||||
},
|
||||
"channels.telegram.threadBindings.idleHours": {
|
||||
label: "Telegram Thread Binding Idle Timeout (hours)",
|
||||
help: "Inactivity window in hours for Telegram bound sessions. Set 0 to disable idle auto-unfocus (default: 24). Overrides session.threadBindings.idleHours when set.",
|
||||
tags: ["network", "storage", "channels"],
|
||||
},
|
||||
"channels.telegram.threadBindings.maxAgeHours": {
|
||||
label: "Telegram Thread Binding Max Age (hours)",
|
||||
help: "Optional hard max age in hours for Telegram bound sessions. Set 0 to disable hard cap (default: 0). Overrides session.threadBindings.maxAgeHours when set.",
|
||||
tags: ["network", "performance", "storage", "channels"],
|
||||
},
|
||||
"channels.telegram.threadBindings.spawnSubagentSessions": {
|
||||
label: "Telegram Thread-Bound Subagent Spawn",
|
||||
help: "Allow subagent spawns with thread=true to auto-bind Telegram current conversations when supported.",
|
||||
tags: ["network", "storage", "channels"],
|
||||
},
|
||||
"channels.telegram.threadBindings.spawnAcpSessions": {
|
||||
label: "Telegram Thread-Bound ACP Spawn",
|
||||
help: "Allow ACP spawns with thread=true to auto-bind Telegram current conversations when supported.",
|
||||
tags: ["network", "storage", "channels"],
|
||||
},
|
||||
"channels.whatsapp.dmPolicy": {
|
||||
label: "WhatsApp DM Policy",
|
||||
help: 'Direct message access control ("pairing" recommended). "open" requires channels.whatsapp.allowFrom=["*"].',
|
||||
tags: ["access", "network", "channels"],
|
||||
},
|
||||
"channels.whatsapp.selfChatMode": {
|
||||
label: "WhatsApp Self-Phone Mode",
|
||||
help: "Same-phone setup (bot uses your personal WhatsApp number).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.whatsapp.debounceMs": {
|
||||
label: "WhatsApp Message Debounce (ms)",
|
||||
help: "Debounce window (ms) for batching rapid consecutive messages from the same sender (0 to disable).",
|
||||
tags: ["network", "performance", "channels"],
|
||||
},
|
||||
"channels.whatsapp.configWrites": {
|
||||
label: "WhatsApp Config Writes",
|
||||
help: "Allow WhatsApp to write config in response to channel events/commands (default: true).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.signal.dmPolicy": {
|
||||
label: "Signal DM Policy",
|
||||
help: 'Direct message access control ("pairing" recommended). "open" requires channels.signal.allowFrom=["*"].',
|
||||
tags: ["access", "network", "channels"],
|
||||
},
|
||||
"channels.signal.configWrites": {
|
||||
label: "Signal Config Writes",
|
||||
help: "Allow Signal to write config in response to channel events/commands (default: true).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.imessage.dmPolicy": {
|
||||
label: "iMessage DM Policy",
|
||||
help: 'Direct message access control ("pairing" recommended). "open" requires channels.imessage.allowFrom=["*"].',
|
||||
tags: ["access", "network", "channels"],
|
||||
},
|
||||
"channels.imessage.configWrites": {
|
||||
label: "iMessage Config Writes",
|
||||
help: "Allow iMessage to write config in response to channel events/commands (default: true).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.bluebubbles.dmPolicy": {
|
||||
label: "BlueBubbles DM Policy",
|
||||
help: 'Direct message access control ("pairing" recommended). "open" requires channels.bluebubbles.allowFrom=["*"].',
|
||||
tags: ["access", "network", "channels"],
|
||||
},
|
||||
"channels.msteams.configWrites": {
|
||||
label: "MS Teams Config Writes",
|
||||
help: "Allow Microsoft Teams to write config in response to channel events/commands (default: true).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.irc.configWrites": {
|
||||
label: "IRC Config Writes",
|
||||
help: "Allow IRC to write config in response to channel events/commands (default: true).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.discord.dmPolicy": {
|
||||
label: "Discord DM Policy",
|
||||
help: 'Direct message access control ("pairing" recommended). "open" requires channels.discord.allowFrom=["*"].',
|
||||
tags: ["access", "network", "channels"],
|
||||
},
|
||||
"channels.discord.dm.policy": {
|
||||
label: "Discord DM Policy",
|
||||
help: 'Direct message access control ("pairing" recommended). "open" requires channels.discord.allowFrom=["*"] (legacy: channels.discord.dm.allowFrom).',
|
||||
tags: ["access", "network", "channels"],
|
||||
},
|
||||
"channels.discord.configWrites": {
|
||||
label: "Discord Config Writes",
|
||||
help: "Allow Discord to write config in response to channel events/commands (default: true).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.discord.proxy": {
|
||||
label: "Discord Proxy URL",
|
||||
help: "Proxy URL for Discord gateway + API requests (app-id lookup and allowlist resolution). Set per account via channels.discord.accounts.<id>.proxy.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.discord.commands.native": {
|
||||
label: "Discord Native Commands",
|
||||
help: 'Override native commands for Discord (bool or "auto").',
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.discord.commands.nativeSkills": {
|
||||
label: "Discord Native Skill Commands",
|
||||
help: 'Override native skill commands for Discord (bool or "auto").',
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.discord.streaming": {
|
||||
label: "Discord Streaming Mode",
|
||||
help: 'Unified Discord stream preview mode: "off" | "partial" | "block" | "progress". "progress" maps to "partial" on Discord. Legacy boolean/streamMode keys are auto-mapped.',
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.discord.streamMode": {
|
||||
label: "Discord Stream Mode (Legacy)",
|
||||
help: "Legacy Discord preview mode alias (off | partial | block); auto-migrated to channels.discord.streaming.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.discord.draftChunk.minChars": {
|
||||
label: "Discord Draft Chunk Min Chars",
|
||||
help: 'Minimum chars before emitting a Discord stream preview update when channels.discord.streaming="block" (default: 200).',
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.discord.draftChunk.maxChars": {
|
||||
label: "Discord Draft Chunk Max Chars",
|
||||
help: 'Target max size for a Discord stream preview chunk when channels.discord.streaming="block" (default: 800; clamped to channels.discord.textChunkLimit).',
|
||||
tags: ["network", "performance", "channels"],
|
||||
},
|
||||
"channels.discord.draftChunk.breakPreference": {
|
||||
label: "Discord Draft Chunk Break Preference",
|
||||
help: "Preferred breakpoints for Discord draft chunks (paragraph | newline | sentence). Default: paragraph.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.discord.retry.attempts": {
|
||||
label: "Discord Retry Attempts",
|
||||
help: "Max retry attempts for outbound Discord API calls (default: 3).",
|
||||
tags: ["network", "reliability", "channels"],
|
||||
},
|
||||
"channels.discord.retry.minDelayMs": {
|
||||
label: "Discord Retry Min Delay (ms)",
|
||||
help: "Minimum retry delay in ms for Discord outbound calls.",
|
||||
tags: ["network", "reliability", "channels"],
|
||||
},
|
||||
"channels.discord.retry.maxDelayMs": {
|
||||
label: "Discord Retry Max Delay (ms)",
|
||||
help: "Maximum retry delay cap in ms for Discord outbound calls.",
|
||||
tags: ["network", "reliability", "performance", "channels"],
|
||||
},
|
||||
"channels.discord.retry.jitter": {
|
||||
label: "Discord Retry Jitter",
|
||||
help: "Jitter factor (0-1) applied to Discord retry delays.",
|
||||
tags: ["network", "reliability", "channels"],
|
||||
},
|
||||
"channels.discord.maxLinesPerMessage": {
|
||||
label: "Discord Max Lines Per Message",
|
||||
help: "Soft max line count per Discord message (default: 17).",
|
||||
tags: ["network", "performance", "channels"],
|
||||
},
|
||||
"channels.discord.inboundWorker.runTimeoutMs": {
|
||||
label: "Discord Inbound Worker Timeout (ms)",
|
||||
help: "Optional queued Discord inbound worker timeout in ms. This is separate from Carbon listener timeouts; defaults to 1800000 and can be disabled with 0. Set per account via channels.discord.accounts.<id>.inboundWorker.runTimeoutMs.",
|
||||
tags: ["network", "performance", "channels"],
|
||||
},
|
||||
"channels.discord.eventQueue.listenerTimeout": {
|
||||
label: "Discord EventQueue Listener Timeout (ms)",
|
||||
help: "Canonical Discord listener timeout control in ms for gateway normalization/enqueue handlers. Default is 120000 in OpenClaw; set per account via channels.discord.accounts.<id>.eventQueue.listenerTimeout.",
|
||||
tags: ["network", "performance", "channels"],
|
||||
},
|
||||
"channels.discord.eventQueue.maxQueueSize": {
|
||||
label: "Discord EventQueue Max Queue Size",
|
||||
help: "Optional Discord EventQueue capacity override (max queued events before backpressure). Set per account via channels.discord.accounts.<id>.eventQueue.maxQueueSize.",
|
||||
tags: ["network", "performance", "channels"],
|
||||
},
|
||||
"channels.discord.eventQueue.maxConcurrency": {
|
||||
label: "Discord EventQueue Max Concurrency",
|
||||
help: "Optional Discord EventQueue concurrency override (max concurrent handler executions). Set per account via channels.discord.accounts.<id>.eventQueue.maxConcurrency.",
|
||||
tags: ["network", "performance", "channels"],
|
||||
},
|
||||
"channels.discord.threadBindings.enabled": {
|
||||
label: "Discord Thread Binding Enabled",
|
||||
help: "Enable Discord thread binding features (/focus, bound-thread routing/delivery, and thread-bound subagent sessions). Overrides session.threadBindings.enabled when set.",
|
||||
tags: ["network", "storage", "channels"],
|
||||
},
|
||||
"channels.discord.threadBindings.idleHours": {
|
||||
label: "Discord Thread Binding Idle Timeout (hours)",
|
||||
help: "Inactivity window in hours for Discord thread-bound sessions (/focus and spawned thread sessions). Set 0 to disable idle auto-unfocus (default: 24). Overrides session.threadBindings.idleHours when set.",
|
||||
tags: ["network", "storage", "channels"],
|
||||
},
|
||||
"channels.discord.threadBindings.maxAgeHours": {
|
||||
label: "Discord Thread Binding Max Age (hours)",
|
||||
help: "Optional hard max age in hours for Discord thread-bound sessions. Set 0 to disable hard cap (default: 0). Overrides session.threadBindings.maxAgeHours when set.",
|
||||
tags: ["network", "performance", "storage", "channels"],
|
||||
},
|
||||
"channels.discord.threadBindings.spawnSubagentSessions": {
|
||||
label: "Discord Thread-Bound Subagent Spawn",
|
||||
help: "Allow subagent spawns with thread=true to auto-create and bind Discord threads (default: false; opt-in). Set true to enable thread-bound subagent spawns for this account/channel.",
|
||||
tags: ["network", "storage", "channels"],
|
||||
},
|
||||
"channels.discord.threadBindings.spawnAcpSessions": {
|
||||
label: "Discord Thread-Bound ACP Spawn",
|
||||
help: "Allow /acp spawn to auto-create and bind Discord threads for ACP sessions (default: false; opt-in). Set true to enable thread-bound ACP spawns for this account/channel.",
|
||||
tags: ["network", "storage", "channels"],
|
||||
},
|
||||
"channels.discord.ui.components.accentColor": {
|
||||
label: "Discord Component Accent Color",
|
||||
help: "Accent color for Discord component containers (hex). Set per account via channels.discord.accounts.<id>.ui.components.accentColor.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.discord.intents.presence": {
|
||||
label: "Discord Presence Intent",
|
||||
help: "Enable the Guild Presences privileged intent. Must also be enabled in the Discord Developer Portal. Allows tracking user activities (e.g. Spotify). Default: false.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.discord.intents.guildMembers": {
|
||||
label: "Discord Guild Members Intent",
|
||||
help: "Enable the Guild Members privileged intent. Must also be enabled in the Discord Developer Portal. Default: false.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.discord.voice.enabled": {
|
||||
label: "Discord Voice Enabled",
|
||||
help: "Enable Discord voice channel conversations (default: true). Omit channels.discord.voice to keep voice support disabled for the account.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.discord.voice.autoJoin": {
|
||||
label: "Discord Voice Auto-Join",
|
||||
help: "Voice channels to auto-join on startup (list of guildId/channelId entries).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.discord.voice.daveEncryption": {
|
||||
label: "Discord Voice DAVE Encryption",
|
||||
help: "Toggle DAVE end-to-end encryption for Discord voice joins (default: true in @discordjs/voice; Discord may require this).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.discord.voice.decryptionFailureTolerance": {
|
||||
label: "Discord Voice Decrypt Failure Tolerance",
|
||||
help: "Consecutive decrypt failures before DAVE attempts session recovery (passed to @discordjs/voice; default: 24).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.discord.voice.tts": {
|
||||
label: "Discord Voice Text-to-Speech",
|
||||
help: "Optional TTS overrides for Discord voice playback (merged with messages.tts).",
|
||||
tags: ["network", "media", "channels"],
|
||||
},
|
||||
"channels.discord.pluralkit.enabled": {
|
||||
label: "Discord PluralKit Enabled",
|
||||
help: "Resolve PluralKit proxied messages and treat system members as distinct senders.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.discord.pluralkit.token": {
|
||||
label: "Discord PluralKit Token",
|
||||
help: "Optional PluralKit token for resolving private systems or members.",
|
||||
tags: ["security", "auth", "network", "channels"],
|
||||
sensitive: true,
|
||||
},
|
||||
"channels.discord.activity": {
|
||||
label: "Discord Presence Activity",
|
||||
help: "Discord presence activity text (defaults to custom status).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.discord.status": {
|
||||
label: "Discord Presence Status",
|
||||
help: "Discord presence status (online, dnd, idle, invisible).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.discord.autoPresence.enabled": {
|
||||
label: "Discord Auto Presence Enabled",
|
||||
help: "Enable automatic Discord bot presence updates based on runtime/model availability signals. When enabled: healthy=>online, degraded/unknown=>idle, exhausted/unavailable=>dnd.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.discord.autoPresence.intervalMs": {
|
||||
label: "Discord Auto Presence Check Interval (ms)",
|
||||
help: "How often to evaluate Discord auto-presence state in milliseconds (default: 30000).",
|
||||
tags: ["network", "performance", "channels"],
|
||||
},
|
||||
"channels.discord.autoPresence.minUpdateIntervalMs": {
|
||||
label: "Discord Auto Presence Min Update Interval (ms)",
|
||||
help: "Minimum time between actual Discord presence update calls in milliseconds (default: 15000). Prevents status spam on noisy state changes.",
|
||||
tags: ["network", "performance", "channels"],
|
||||
},
|
||||
"channels.discord.autoPresence.healthyText": {
|
||||
label: "Discord Auto Presence Healthy Text",
|
||||
help: "Optional custom status text while runtime is healthy (online). If omitted, falls back to static channels.discord.activity when set.",
|
||||
tags: ["network", "reliability", "channels"],
|
||||
},
|
||||
"channels.discord.autoPresence.degradedText": {
|
||||
label: "Discord Auto Presence Degraded Text",
|
||||
help: "Optional custom status text while runtime/model availability is degraded or unknown (idle).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.discord.autoPresence.exhaustedText": {
|
||||
label: "Discord Auto Presence Exhausted Text",
|
||||
help: "Optional custom status text while runtime detects exhausted/unavailable model quota (dnd). Supports {reason} template placeholder.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.discord.activityType": {
|
||||
label: "Discord Presence Activity Type",
|
||||
help: "Discord presence activity type (0=Playing,1=Streaming,2=Listening,3=Watching,4=Custom,5=Competing).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.discord.activityUrl": {
|
||||
label: "Discord Presence Activity URL",
|
||||
help: "Discord presence streaming URL (required for activityType=1).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.slack.dm.policy": {
|
||||
label: "Slack DM Policy",
|
||||
help: 'Direct message access control ("pairing" recommended). "open" requires channels.slack.allowFrom=["*"] (legacy: channels.slack.dm.allowFrom).',
|
||||
tags: ["access", "network", "channels"],
|
||||
},
|
||||
"channels.slack.dmPolicy": {
|
||||
label: "Slack DM Policy",
|
||||
help: 'Direct message access control ("pairing" recommended). "open" requires channels.slack.allowFrom=["*"].',
|
||||
tags: ["access", "network", "channels"],
|
||||
},
|
||||
"channels.slack.configWrites": {
|
||||
label: "Slack Config Writes",
|
||||
help: "Allow Slack to write config in response to channel events/commands (default: true).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.slack.commands.native": {
|
||||
label: "Slack Native Commands",
|
||||
help: 'Override native commands for Slack (bool or "auto").',
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.slack.commands.nativeSkills": {
|
||||
label: "Slack Native Skill Commands",
|
||||
help: 'Override native skill commands for Slack (bool or "auto").',
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.slack.allowBots": {
|
||||
label: "Slack Allow Bot Messages",
|
||||
help: "Allow bot-authored messages to trigger Slack replies (default: false).",
|
||||
tags: ["access", "network", "channels"],
|
||||
},
|
||||
"channels.discord.allowBots": {
|
||||
label: "Discord Allow Bot Messages",
|
||||
help: 'Allow bot-authored messages to trigger Discord replies (default: false). Set "mentions" to only accept bot messages that mention the bot.',
|
||||
tags: ["access", "network", "channels"],
|
||||
},
|
||||
"channels.matrix.allowBots": {
|
||||
label: "Matrix Allow Bot Messages",
|
||||
help: 'Allow messages from other configured Matrix bot accounts to trigger replies (default: false). Set "mentions" to only accept bot messages that visibly mention this bot.',
|
||||
tags: ["access", "network", "channels"],
|
||||
},
|
||||
"channels.discord.token": {
|
||||
label: "Discord Bot Token",
|
||||
help: "Discord bot token used for gateway and REST API authentication for this provider account. Keep this secret out of committed config and rotate immediately after any leak.",
|
||||
tags: ["security", "auth", "network", "channels"],
|
||||
sensitive: true,
|
||||
},
|
||||
"channels.slack.botToken": {
|
||||
label: "Slack Bot Token",
|
||||
help: "Slack bot token used for standard chat actions in the configured workspace. Keep this credential scoped and rotate if workspace app permissions change.",
|
||||
tags: ["security", "auth", "network", "channels"],
|
||||
sensitive: true,
|
||||
},
|
||||
"channels.slack.appToken": {
|
||||
label: "Slack App Token",
|
||||
help: "Slack app-level token used for Socket Mode connections and event transport when enabled. Use least-privilege app scopes and store this token as a secret.",
|
||||
tags: ["security", "auth", "network", "channels"],
|
||||
sensitive: true,
|
||||
},
|
||||
"channels.slack.userToken": {
|
||||
label: "Slack User Token",
|
||||
help: "Optional Slack user token for workflows requiring user-context API access beyond bot permissions. Use sparingly and audit scopes because this token can carry broader authority.",
|
||||
tags: ["security", "auth", "network", "channels"],
|
||||
sensitive: true,
|
||||
},
|
||||
"channels.slack.userTokenReadOnly": {
|
||||
label: "Slack User Token Read Only",
|
||||
help: "When true, treat configured Slack user token usage as read-only helper behavior where possible. Keep enabled if you only need supplemental reads without user-context writes.",
|
||||
tags: ["security", "auth", "network", "channels"],
|
||||
},
|
||||
"channels.slack.capabilities.interactiveReplies": {
|
||||
label: "Slack Interactive Replies",
|
||||
help: "Enable agent-authored Slack interactive reply directives (`[[slack_buttons: ...]]`, `[[slack_select: ...]]`). Default: false.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.slack.streaming": {
|
||||
label: "Slack Streaming Mode",
|
||||
help: 'Unified Slack stream preview mode: "off" | "partial" | "block" | "progress". Legacy boolean/streamMode keys are auto-mapped.',
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.slack.nativeStreaming": {
|
||||
label: "Slack Native Streaming",
|
||||
help: "Enable native Slack text streaming (chat.startStream/chat.appendStream/chat.stopStream) when channels.slack.streaming is partial (default: true).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.slack.streamMode": {
|
||||
label: "Slack Stream Mode (Legacy)",
|
||||
help: "Legacy Slack preview mode alias (replace | status_final | append); auto-migrated to channels.slack.streaming.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.slack.thread.historyScope": {
|
||||
label: "Slack Thread History Scope",
|
||||
help: 'Scope for Slack thread history context ("thread" isolates per thread; "channel" reuses channel history).',
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.slack.thread.inheritParent": {
|
||||
label: "Slack Thread Parent Inheritance",
|
||||
help: "If true, Slack thread sessions inherit the parent channel transcript (default: false).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.slack.thread.initialHistoryLimit": {
|
||||
label: "Slack Thread Initial History Limit",
|
||||
help: "Maximum number of existing Slack thread messages to fetch when starting a new thread session (default: 20, set to 0 to disable).",
|
||||
tags: ["network", "performance", "channels"],
|
||||
},
|
||||
"channels.mattermost.botToken": {
|
||||
label: "Mattermost Bot Token",
|
||||
help: "Bot token from Mattermost System Console -> Integrations -> Bot Accounts.",
|
||||
tags: ["security", "auth", "network", "channels"],
|
||||
},
|
||||
"channels.mattermost.baseUrl": {
|
||||
label: "Mattermost Base URL",
|
||||
help: "Base URL for your Mattermost server (e.g., https://chat.example.com).",
|
||||
placeholder: "https://chat.example.com",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.mattermost.configWrites": {
|
||||
label: "Mattermost Config Writes",
|
||||
help: "Allow Mattermost to write config in response to channel events/commands (default: true).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.mattermost.chatmode": {
|
||||
label: "Mattermost Chat Mode",
|
||||
help: 'Reply to channel messages on mention ("oncall"), on trigger chars (">" or "!") ("onchar"), or on every message ("onmessage").',
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.mattermost.oncharPrefixes": {
|
||||
label: "Mattermost Onchar Prefixes",
|
||||
help: 'Trigger prefixes for onchar mode (default: [">", "!"]).',
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.mattermost.requireMention": {
|
||||
label: "Mattermost Require Mention",
|
||||
help: "Require @mention in channels before responding (default: true).",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.signal.account": {
|
||||
label: "Signal Account",
|
||||
help: "Signal account identifier (phone/number handle) used to bind this channel config to a specific Signal identity. Keep this aligned with your linked device/session state.",
|
||||
tags: ["network", "channels"],
|
||||
},
|
||||
"channels.imessage.cliPath": {
|
||||
label: "iMessage CLI Path",
|
||||
help: "Filesystem path to the iMessage bridge CLI binary used for send/receive operations. Set explicitly when the binary is not on PATH in service runtime environments.",
|
||||
tags: ["network", "storage", "channels"],
|
||||
},
|
||||
"agents.list[].skills": {
|
||||
label: "Agent Skill Filter",
|
||||
help: "Optional allowlist of skills for this agent (omit = all skills; empty = no skills).",
|
||||
@@ -15915,94 +15251,6 @@ export const GENERATED_BASE_CONFIG_SCHEMA = {
|
||||
sensitive: true,
|
||||
tags: ["security", "auth", "tools"],
|
||||
},
|
||||
"channels.telegram.webhookSecret": {
|
||||
sensitive: true,
|
||||
tags: ["security", "auth", "network", "channels"],
|
||||
},
|
||||
"channels.telegram.accounts.*.botToken": {
|
||||
sensitive: true,
|
||||
tags: ["security", "auth", "network", "channels"],
|
||||
},
|
||||
"channels.telegram.accounts.*.webhookSecret": {
|
||||
sensitive: true,
|
||||
tags: ["security", "auth", "network", "channels"],
|
||||
},
|
||||
"channels.discord.voice.tts.providers.*.apiKey": {
|
||||
sensitive: true,
|
||||
tags: ["security", "auth", "network", "media", "channels"],
|
||||
},
|
||||
"channels.discord.accounts.*.token": {
|
||||
sensitive: true,
|
||||
tags: ["security", "auth", "network", "channels"],
|
||||
},
|
||||
"channels.discord.accounts.*.voice.tts.providers.*.apiKey": {
|
||||
sensitive: true,
|
||||
tags: ["security", "auth", "network", "media", "channels"],
|
||||
},
|
||||
"channels.discord.accounts.*.pluralkit.token": {
|
||||
sensitive: true,
|
||||
tags: ["security", "auth", "network", "channels"],
|
||||
},
|
||||
"channels.irc.password": {
|
||||
sensitive: true,
|
||||
tags: ["security", "auth", "network", "channels"],
|
||||
},
|
||||
"channels.irc.accounts.*.password": {
|
||||
sensitive: true,
|
||||
tags: ["security", "auth", "network", "channels"],
|
||||
},
|
||||
"channels.irc.accounts.*.nickserv.password": {
|
||||
sensitive: true,
|
||||
tags: ["security", "auth", "network", "channels"],
|
||||
},
|
||||
"channels.googlechat.serviceAccount": {
|
||||
sensitive: true,
|
||||
tags: ["security", "network", "channels"],
|
||||
},
|
||||
"channels.googlechat.serviceAccountRef": {
|
||||
sensitive: true,
|
||||
tags: ["security", "network", "channels"],
|
||||
},
|
||||
"channels.googlechat.accounts.*.serviceAccount": {
|
||||
sensitive: true,
|
||||
tags: ["security", "network", "channels"],
|
||||
},
|
||||
"channels.googlechat.accounts.*.serviceAccountRef": {
|
||||
sensitive: true,
|
||||
tags: ["security", "network", "channels"],
|
||||
},
|
||||
"channels.slack.signingSecret": {
|
||||
sensitive: true,
|
||||
tags: ["security", "auth", "network", "channels"],
|
||||
},
|
||||
"channels.slack.accounts.*.signingSecret": {
|
||||
sensitive: true,
|
||||
tags: ["security", "auth", "network", "channels"],
|
||||
},
|
||||
"channels.slack.accounts.*.botToken": {
|
||||
sensitive: true,
|
||||
tags: ["security", "auth", "network", "channels"],
|
||||
},
|
||||
"channels.slack.accounts.*.appToken": {
|
||||
sensitive: true,
|
||||
tags: ["security", "auth", "network", "channels"],
|
||||
},
|
||||
"channels.slack.accounts.*.userToken": {
|
||||
sensitive: true,
|
||||
tags: ["security", "auth", "network", "channels"],
|
||||
},
|
||||
"channels.bluebubbles.password": {
|
||||
sensitive: true,
|
||||
tags: ["security", "auth", "network", "channels"],
|
||||
},
|
||||
"channels.bluebubbles.accounts.*.password": {
|
||||
sensitive: true,
|
||||
tags: ["security", "auth", "network", "channels"],
|
||||
},
|
||||
"channels.msteams.appPassword": {
|
||||
sensitive: true,
|
||||
tags: ["security", "auth", "network", "channels"],
|
||||
},
|
||||
"skills.entries.*.apiKey": {
|
||||
sensitive: true,
|
||||
tags: ["security", "auth"],
|
||||
|
||||
@@ -517,31 +517,7 @@ const CHANNELS_AGENTS_TARGET_KEYS = [
|
||||
"agents.list[].tools.alsoAllow",
|
||||
"agents.list[].tools.byProvider",
|
||||
"agents.list[].tools.profile",
|
||||
"channels.bluebubbles",
|
||||
"channels.discord",
|
||||
"channels.discord.token",
|
||||
"channels.imessage",
|
||||
"channels.imessage.cliPath",
|
||||
"channels.irc",
|
||||
"channels.mattermost",
|
||||
"channels.msteams",
|
||||
"channels.signal",
|
||||
"channels.signal.account",
|
||||
"channels.slack",
|
||||
"channels.slack.appToken",
|
||||
"channels.slack.botToken",
|
||||
"channels.slack.userToken",
|
||||
"channels.slack.userTokenReadOnly",
|
||||
"channels.telegram",
|
||||
"channels.telegram.botToken",
|
||||
"channels.telegram.capabilities.inlineButtons",
|
||||
"channels.telegram.execApprovals",
|
||||
"channels.telegram.execApprovals.enabled",
|
||||
"channels.telegram.execApprovals.approvers",
|
||||
"channels.telegram.execApprovals.agentFilter",
|
||||
"channels.telegram.execApprovals.sessionFilter",
|
||||
"channels.telegram.execApprovals.target",
|
||||
"channels.whatsapp",
|
||||
] as const;
|
||||
|
||||
const FINAL_BACKLOG_TARGET_KEYS = [
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
DISCORD_DEFAULT_INBOUND_WORKER_TIMEOUT_MS,
|
||||
DISCORD_DEFAULT_LISTENER_TIMEOUT_MS,
|
||||
} from "../../extensions/discord/timeouts.js";
|
||||
import { MEDIA_AUDIO_FIELD_HELP } from "./media-audio-field-metadata.js";
|
||||
import { IRC_FIELD_HELP } from "./schema.irc.js";
|
||||
import { describeTalkSilenceTimeoutDefaults } from "./talk-defaults.js";
|
||||
|
||||
export const FIELD_HELP: Record<string, string> = {
|
||||
@@ -736,16 +731,8 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"models.bedrockDiscovery.defaultMaxTokens":
|
||||
"Fallback max-token value applied to discovered models without explicit output token limits. Use conservative defaults to reduce truncation surprises and unexpected token spend.",
|
||||
auth: "Authentication profile root used for multi-profile provider credentials and cooldown-based failover ordering. Keep profiles minimal and explicit so automatic failover behavior stays auditable.",
|
||||
"channels.slack.allowBots":
|
||||
"Allow bot-authored messages to trigger Slack replies (default: false).",
|
||||
"channels.matrix.allowBots":
|
||||
'Allow messages from other configured Matrix bot accounts to trigger replies (default: false). Set "mentions" to only accept bot messages that visibly mention this bot.',
|
||||
"channels.slack.thread.historyScope":
|
||||
'Scope for Slack thread history context ("thread" isolates per thread; "channel" reuses channel history).',
|
||||
"channels.slack.thread.inheritParent":
|
||||
"If true, Slack thread sessions inherit the parent channel transcript (default: false).",
|
||||
"channels.slack.thread.initialHistoryLimit":
|
||||
"Maximum number of existing Slack thread messages to fetch when starting a new thread session (default: 20, set to 0 to disable).",
|
||||
"channels.mattermost.botToken":
|
||||
"Bot token from Mattermost System Console -> Integrations -> Bot Accounts.",
|
||||
"channels.mattermost.baseUrl":
|
||||
@@ -1398,26 +1385,8 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"Provider API key used by that speech provider when its plugin requires authenticated TTS access.", // pragma: allowlist secret
|
||||
channels:
|
||||
"Channel provider configurations plus shared defaults that control access policies, heartbeat visibility, and per-surface behavior. Keep defaults centralized and override per provider only where required.",
|
||||
"channels.telegram":
|
||||
"Telegram channel provider configuration including auth tokens, retry behavior, and message rendering controls. Use this section to tune bot behavior for Telegram-specific API semantics.",
|
||||
"channels.slack":
|
||||
"Slack channel provider configuration for bot/app tokens, streaming behavior, and DM policy controls. Keep token handling and thread behavior explicit to avoid noisy workspace interactions.",
|
||||
"channels.discord":
|
||||
"Discord channel provider configuration for bot auth, retry policy, streaming, thread bindings, and optional voice capabilities. Keep privileged intents and advanced features disabled unless needed.",
|
||||
"channels.whatsapp":
|
||||
"WhatsApp channel provider configuration for access policy and message batching behavior. Use this section to tune responsiveness and direct-message routing safety for WhatsApp chats.",
|
||||
"channels.signal":
|
||||
"Signal channel provider configuration including account identity and DM policy behavior. Keep account mapping explicit so routing remains stable across multi-device setups.",
|
||||
"channels.imessage":
|
||||
"iMessage channel provider configuration for CLI integration and DM access policy handling. Use explicit CLI paths when runtime environments have non-standard binary locations.",
|
||||
"channels.bluebubbles":
|
||||
"BlueBubbles channel provider configuration used for Apple messaging bridge integrations. Keep DM policy aligned with your trusted sender model in shared deployments.",
|
||||
"channels.msteams":
|
||||
"Microsoft Teams channel provider configuration and provider-specific policy toggles. Use this section to isolate Teams behavior from other enterprise chat providers.",
|
||||
"channels.mattermost":
|
||||
"Mattermost channel provider configuration for bot credentials, base URL, and message trigger modes. Keep mention/trigger rules strict in high-volume team channels.",
|
||||
"channels.irc":
|
||||
"IRC channel provider configuration and compatibility settings for classic IRC transport workflows. Use this section when bridging legacy chat infrastructure into OpenClaw.",
|
||||
"channels.defaults":
|
||||
"Default channel behavior applied across providers when provider-specific settings are not set. Use this to enforce consistent baseline policy before per-provider tuning.",
|
||||
"channels.defaults.groupPolicy":
|
||||
@@ -1434,78 +1403,10 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
'Controls whether heartbeat delivery may target direct/DM chats: "allow" (default) permits DM delivery and "block" suppresses direct-target sends.',
|
||||
"agents.list.*.heartbeat.directPolicy":
|
||||
'Per-agent override for heartbeat direct/DM delivery policy; use "block" for agents that should only send heartbeat alerts to non-DM destinations.',
|
||||
"channels.telegram.configWrites":
|
||||
"Allow Telegram to write config in response to channel events/commands (default: true).",
|
||||
"channels.telegram.botToken":
|
||||
"Telegram bot token used to authenticate Bot API requests for this account/provider config. Use secret/env substitution and rotate tokens if exposure is suspected.",
|
||||
"channels.telegram.capabilities.inlineButtons":
|
||||
"Enable Telegram inline button components for supported command and interaction surfaces. Disable if your deployment needs plain-text-only compatibility behavior.",
|
||||
"channels.telegram.execApprovals":
|
||||
"Telegram-native exec approval routing and approver authorization. Enable this only when Telegram should act as an explicit exec-approval client for the selected bot account.",
|
||||
"channels.telegram.execApprovals.enabled":
|
||||
"Enable Telegram exec approvals for this account. When false or unset, Telegram messages/buttons cannot approve exec requests.",
|
||||
"channels.telegram.execApprovals.approvers":
|
||||
"Telegram user IDs allowed to approve exec requests for this bot account. Use numeric Telegram user IDs; prompts are only delivered to these approvers when target includes dm.",
|
||||
"channels.telegram.execApprovals.agentFilter":
|
||||
'Optional allowlist of agent IDs eligible for Telegram exec approvals, for example `["main", "ops-agent"]`. Use this to keep approval prompts scoped to the agents you actually operate from Telegram.',
|
||||
"channels.telegram.execApprovals.sessionFilter":
|
||||
"Optional session-key filters matched as substring or regex-style patterns before Telegram approval routing is used. Use narrow patterns so Telegram approvals only appear for intended sessions.",
|
||||
"channels.telegram.execApprovals.target":
|
||||
'Controls where Telegram approval prompts are sent: "dm" sends to approver DMs (default), "channel" sends to the originating Telegram chat/topic, and "both" sends to both. Channel delivery exposes the command text to the chat, so only use it in trusted groups/topics.',
|
||||
"channels.slack.configWrites":
|
||||
"Allow Slack to write config in response to channel events/commands (default: true).",
|
||||
"channels.slack.botToken":
|
||||
"Slack bot token used for standard chat actions in the configured workspace. Keep this credential scoped and rotate if workspace app permissions change.",
|
||||
"channels.slack.appToken":
|
||||
"Slack app-level token used for Socket Mode connections and event transport when enabled. Use least-privilege app scopes and store this token as a secret.",
|
||||
"channels.slack.userToken":
|
||||
"Optional Slack user token for workflows requiring user-context API access beyond bot permissions. Use sparingly and audit scopes because this token can carry broader authority.",
|
||||
"channels.slack.userTokenReadOnly":
|
||||
"When true, treat configured Slack user token usage as read-only helper behavior where possible. Keep enabled if you only need supplemental reads without user-context writes.",
|
||||
"channels.slack.capabilities.interactiveReplies":
|
||||
"Enable agent-authored Slack interactive reply directives (`[[slack_buttons: ...]]`, `[[slack_select: ...]]`). Default: false.",
|
||||
"channels.mattermost.configWrites":
|
||||
"Allow Mattermost to write config in response to channel events/commands (default: true).",
|
||||
"channels.discord.configWrites":
|
||||
"Allow Discord to write config in response to channel events/commands (default: true).",
|
||||
"channels.discord.token":
|
||||
"Discord bot token used for gateway and REST API authentication for this provider account. Keep this secret out of committed config and rotate immediately after any leak.",
|
||||
"channels.discord.allowBots":
|
||||
'Allow bot-authored messages to trigger Discord replies (default: false). Set "mentions" to only accept bot messages that mention the bot.',
|
||||
"channels.discord.proxy":
|
||||
"Proxy URL for Discord gateway + API requests (app-id lookup and allowlist resolution). Set per account via channels.discord.accounts.<id>.proxy.",
|
||||
"channels.whatsapp.configWrites":
|
||||
"Allow WhatsApp to write config in response to channel events/commands (default: true).",
|
||||
"channels.signal.configWrites":
|
||||
"Allow Signal to write config in response to channel events/commands (default: true).",
|
||||
"channels.signal.account":
|
||||
"Signal account identifier (phone/number handle) used to bind this channel config to a specific Signal identity. Keep this aligned with your linked device/session state.",
|
||||
"channels.imessage.configWrites":
|
||||
"Allow iMessage to write config in response to channel events/commands (default: true).",
|
||||
"channels.imessage.cliPath":
|
||||
"Filesystem path to the iMessage bridge CLI binary used for send/receive operations. Set explicitly when the binary is not on PATH in service runtime environments.",
|
||||
"channels.msteams.configWrites":
|
||||
"Allow Microsoft Teams to write config in response to channel events/commands (default: true).",
|
||||
"channels.modelByChannel":
|
||||
"Map provider -> channel id -> model override (values are provider/model or aliases).",
|
||||
...IRC_FIELD_HELP,
|
||||
"channels.discord.commands.native": 'Override native commands for Discord (bool or "auto").',
|
||||
"channels.discord.commands.nativeSkills":
|
||||
'Override native skill commands for Discord (bool or "auto").',
|
||||
"channels.telegram.commands.native": 'Override native commands for Telegram (bool or "auto").',
|
||||
"channels.telegram.commands.nativeSkills":
|
||||
'Override native skill commands for Telegram (bool or "auto").',
|
||||
"channels.slack.commands.native": 'Override native commands for Slack (bool or "auto").',
|
||||
"channels.slack.commands.nativeSkills":
|
||||
'Override native skill commands for Slack (bool or "auto").',
|
||||
"channels.slack.streaming":
|
||||
'Unified Slack stream preview mode: "off" | "partial" | "block" | "progress". Legacy boolean/streamMode keys are auto-mapped.',
|
||||
"channels.slack.nativeStreaming":
|
||||
"Enable native Slack text streaming (chat.startStream/chat.appendStream/chat.stopStream) when channels.slack.streaming is partial (default: true).",
|
||||
"channels.slack.streamMode":
|
||||
"Legacy Slack preview mode alias (replace | status_final | append); auto-migrated to channels.slack.streaming.",
|
||||
"channels.telegram.customCommands":
|
||||
"Additional Telegram bot menu commands (merged with native; conflicts ignored).",
|
||||
"messages.suppressToolErrors":
|
||||
"When true, suppress ⚠️ tool-error warnings from being shown to the user. The agent already sees errors in context and can retry. Default: false.",
|
||||
"messages.ackReaction": "Emoji reaction used to acknowledge inbound messages (empty disables).",
|
||||
@@ -1521,126 +1422,4 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"Override default timing. Keys: debounceMs (700), stallSoftMs (25000), stallHardMs (60000), doneHoldMs (1500), errorHoldMs (2500).",
|
||||
"messages.inbound.debounceMs":
|
||||
"Debounce window (ms) for batching rapid inbound messages from the same sender (0 to disable).",
|
||||
"channels.telegram.dmPolicy":
|
||||
'Direct message access control ("pairing" recommended). "open" requires channels.telegram.allowFrom=["*"].',
|
||||
"channels.telegram.streaming":
|
||||
'Unified Telegram stream preview mode: "off" | "partial" | "block" | "progress" (default: "partial"). "progress" maps to "partial" on Telegram. Legacy boolean/streamMode keys are auto-mapped.',
|
||||
"channels.discord.streaming":
|
||||
'Unified Discord stream preview mode: "off" | "partial" | "block" | "progress". "progress" maps to "partial" on Discord. Legacy boolean/streamMode keys are auto-mapped.',
|
||||
"channels.discord.streamMode":
|
||||
"Legacy Discord preview mode alias (off | partial | block); auto-migrated to channels.discord.streaming.",
|
||||
"channels.discord.draftChunk.minChars":
|
||||
'Minimum chars before emitting a Discord stream preview update when channels.discord.streaming="block" (default: 200).',
|
||||
"channels.discord.draftChunk.maxChars":
|
||||
'Target max size for a Discord stream preview chunk when channels.discord.streaming="block" (default: 800; clamped to channels.discord.textChunkLimit).',
|
||||
"channels.discord.draftChunk.breakPreference":
|
||||
"Preferred breakpoints for Discord draft chunks (paragraph | newline | sentence). Default: paragraph.",
|
||||
"channels.telegram.retry.attempts":
|
||||
"Max retry attempts for outbound Telegram API calls (default: 3).",
|
||||
"channels.telegram.retry.minDelayMs": "Minimum retry delay in ms for Telegram outbound calls.",
|
||||
"channels.telegram.retry.maxDelayMs":
|
||||
"Maximum retry delay cap in ms for Telegram outbound calls.",
|
||||
"channels.telegram.retry.jitter": "Jitter factor (0-1) applied to Telegram retry delays.",
|
||||
"channels.telegram.network.autoSelectFamily":
|
||||
"Override Node autoSelectFamily for Telegram (true=enable, false=disable).",
|
||||
"channels.telegram.timeoutSeconds":
|
||||
"Max seconds before Telegram API requests are aborted (default: 500 per grammY).",
|
||||
"channels.telegram.silentErrorReplies":
|
||||
"When true, Telegram bot replies marked as errors are sent silently (no notification sound). Default: false.",
|
||||
"channels.telegram.apiRoot":
|
||||
"Custom Telegram Bot API root URL. Use for self-hosted Bot API servers (https://github.com/tdlib/telegram-bot-api) or reverse proxies in regions where api.telegram.org is blocked.",
|
||||
"channels.telegram.autoTopicLabel":
|
||||
"Auto-rename DM forum topics on first message using LLM. Default: true. Set to false to disable, or use object form { enabled: true, prompt: '...' } for custom prompt.",
|
||||
"channels.telegram.autoTopicLabel.enabled":
|
||||
"Whether auto topic labeling is enabled. Default: true.",
|
||||
"channels.telegram.autoTopicLabel.prompt":
|
||||
"Custom prompt for LLM-based topic naming. The user message is appended after the prompt.",
|
||||
"channels.telegram.threadBindings.enabled":
|
||||
"Enable Telegram conversation binding features (/focus, /unfocus, /agents, and /session idle|max-age). Overrides session.threadBindings.enabled when set.",
|
||||
"channels.telegram.threadBindings.idleHours":
|
||||
"Inactivity window in hours for Telegram bound sessions. Set 0 to disable idle auto-unfocus (default: 24). Overrides session.threadBindings.idleHours when set.",
|
||||
"channels.telegram.threadBindings.maxAgeHours":
|
||||
"Optional hard max age in hours for Telegram bound sessions. Set 0 to disable hard cap (default: 0). Overrides session.threadBindings.maxAgeHours when set.",
|
||||
"channels.telegram.threadBindings.spawnSubagentSessions":
|
||||
"Allow subagent spawns with thread=true to auto-bind Telegram current conversations when supported.",
|
||||
"channels.telegram.threadBindings.spawnAcpSessions":
|
||||
"Allow ACP spawns with thread=true to auto-bind Telegram current conversations when supported.",
|
||||
"channels.whatsapp.dmPolicy":
|
||||
'Direct message access control ("pairing" recommended). "open" requires channels.whatsapp.allowFrom=["*"].',
|
||||
"channels.whatsapp.selfChatMode": "Same-phone setup (bot uses your personal WhatsApp number).",
|
||||
"channels.whatsapp.debounceMs":
|
||||
"Debounce window (ms) for batching rapid consecutive messages from the same sender (0 to disable).",
|
||||
"channels.signal.dmPolicy":
|
||||
'Direct message access control ("pairing" recommended). "open" requires channels.signal.allowFrom=["*"].',
|
||||
"channels.imessage.dmPolicy":
|
||||
'Direct message access control ("pairing" recommended). "open" requires channels.imessage.allowFrom=["*"].',
|
||||
"channels.bluebubbles.dmPolicy":
|
||||
'Direct message access control ("pairing" recommended). "open" requires channels.bluebubbles.allowFrom=["*"].',
|
||||
"channels.discord.dmPolicy":
|
||||
'Direct message access control ("pairing" recommended). "open" requires channels.discord.allowFrom=["*"].',
|
||||
"channels.discord.dm.policy":
|
||||
'Direct message access control ("pairing" recommended). "open" requires channels.discord.allowFrom=["*"] (legacy: channels.discord.dm.allowFrom).',
|
||||
"channels.discord.retry.attempts":
|
||||
"Max retry attempts for outbound Discord API calls (default: 3).",
|
||||
"channels.discord.retry.minDelayMs": "Minimum retry delay in ms for Discord outbound calls.",
|
||||
"channels.discord.retry.maxDelayMs": "Maximum retry delay cap in ms for Discord outbound calls.",
|
||||
"channels.discord.retry.jitter": "Jitter factor (0-1) applied to Discord retry delays.",
|
||||
"channels.discord.maxLinesPerMessage": "Soft max line count per Discord message (default: 17).",
|
||||
"channels.discord.inboundWorker.runTimeoutMs": `Optional queued Discord inbound worker timeout in ms. This is separate from Carbon listener timeouts; defaults to ${DISCORD_DEFAULT_INBOUND_WORKER_TIMEOUT_MS} and can be disabled with 0. Set per account via channels.discord.accounts.<id>.inboundWorker.runTimeoutMs.`,
|
||||
"channels.discord.eventQueue.listenerTimeout": `Canonical Discord listener timeout control in ms for gateway normalization/enqueue handlers. Default is ${DISCORD_DEFAULT_LISTENER_TIMEOUT_MS} in OpenClaw; set per account via channels.discord.accounts.<id>.eventQueue.listenerTimeout.`,
|
||||
"channels.discord.eventQueue.maxQueueSize":
|
||||
"Optional Discord EventQueue capacity override (max queued events before backpressure). Set per account via channels.discord.accounts.<id>.eventQueue.maxQueueSize.",
|
||||
"channels.discord.eventQueue.maxConcurrency":
|
||||
"Optional Discord EventQueue concurrency override (max concurrent handler executions). Set per account via channels.discord.accounts.<id>.eventQueue.maxConcurrency.",
|
||||
"channels.discord.threadBindings.enabled":
|
||||
"Enable Discord thread binding features (/focus, bound-thread routing/delivery, and thread-bound subagent sessions). Overrides session.threadBindings.enabled when set.",
|
||||
"channels.discord.threadBindings.idleHours":
|
||||
"Inactivity window in hours for Discord thread-bound sessions (/focus and spawned thread sessions). Set 0 to disable idle auto-unfocus (default: 24). Overrides session.threadBindings.idleHours when set.",
|
||||
"channels.discord.threadBindings.maxAgeHours":
|
||||
"Optional hard max age in hours for Discord thread-bound sessions. Set 0 to disable hard cap (default: 0). Overrides session.threadBindings.maxAgeHours when set.",
|
||||
"channels.discord.threadBindings.spawnSubagentSessions":
|
||||
"Allow subagent spawns with thread=true to auto-create and bind Discord threads (default: false; opt-in). Set true to enable thread-bound subagent spawns for this account/channel.",
|
||||
"channels.discord.threadBindings.spawnAcpSessions":
|
||||
"Allow /acp spawn to auto-create and bind Discord threads for ACP sessions (default: false; opt-in). Set true to enable thread-bound ACP spawns for this account/channel.",
|
||||
"channels.discord.ui.components.accentColor":
|
||||
"Accent color for Discord component containers (hex). Set per account via channels.discord.accounts.<id>.ui.components.accentColor.",
|
||||
"channels.discord.voice.enabled":
|
||||
"Enable Discord voice channel conversations (default: true). Omit channels.discord.voice to keep voice support disabled for the account.",
|
||||
"channels.discord.voice.autoJoin":
|
||||
"Voice channels to auto-join on startup (list of guildId/channelId entries).",
|
||||
"channels.discord.voice.daveEncryption":
|
||||
"Toggle DAVE end-to-end encryption for Discord voice joins (default: true in @discordjs/voice; Discord may require this).",
|
||||
"channels.discord.voice.decryptionFailureTolerance":
|
||||
"Consecutive decrypt failures before DAVE attempts session recovery (passed to @discordjs/voice; default: 24).",
|
||||
"channels.discord.voice.tts":
|
||||
"Optional TTS overrides for Discord voice playback (merged with messages.tts).",
|
||||
"channels.discord.intents.presence":
|
||||
"Enable the Guild Presences privileged intent. Must also be enabled in the Discord Developer Portal. Allows tracking user activities (e.g. Spotify). Default: false.",
|
||||
"channels.discord.intents.guildMembers":
|
||||
"Enable the Guild Members privileged intent. Must also be enabled in the Discord Developer Portal. Default: false.",
|
||||
"channels.discord.pluralkit.enabled":
|
||||
"Resolve PluralKit proxied messages and treat system members as distinct senders.",
|
||||
"channels.discord.pluralkit.token":
|
||||
"Optional PluralKit token for resolving private systems or members.",
|
||||
"channels.discord.activity": "Discord presence activity text (defaults to custom status).",
|
||||
"channels.discord.status": "Discord presence status (online, dnd, idle, invisible).",
|
||||
"channels.discord.autoPresence.enabled":
|
||||
"Enable automatic Discord bot presence updates based on runtime/model availability signals. When enabled: healthy=>online, degraded/unknown=>idle, exhausted/unavailable=>dnd.",
|
||||
"channels.discord.autoPresence.intervalMs":
|
||||
"How often to evaluate Discord auto-presence state in milliseconds (default: 30000).",
|
||||
"channels.discord.autoPresence.minUpdateIntervalMs":
|
||||
"Minimum time between actual Discord presence update calls in milliseconds (default: 15000). Prevents status spam on noisy state changes.",
|
||||
"channels.discord.autoPresence.healthyText":
|
||||
"Optional custom status text while runtime is healthy (online). If omitted, falls back to static channels.discord.activity when set.",
|
||||
"channels.discord.autoPresence.degradedText":
|
||||
"Optional custom status text while runtime/model availability is degraded or unknown (idle).",
|
||||
"channels.discord.autoPresence.exhaustedText":
|
||||
"Optional custom status text while runtime detects exhausted/unavailable model quota (dnd). Supports {reason} template placeholder.",
|
||||
"channels.discord.activityType":
|
||||
"Discord presence activity type (0=Playing,1=Streaming,2=Listening,3=Watching,4=Custom,5=Competing).",
|
||||
"channels.discord.activityUrl": "Discord presence streaming URL (required for activityType=1).",
|
||||
"channels.slack.dm.policy":
|
||||
'Direct message access control ("pairing" recommended). "open" requires channels.slack.allowFrom=["*"] (legacy: channels.slack.dm.allowFrom).',
|
||||
"channels.slack.dmPolicy":
|
||||
'Direct message access control ("pairing" recommended). "open" requires channels.slack.allowFrom=["*"].',
|
||||
};
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { buildSecretInputSchema } from "../plugin-sdk/secret-input-schema.js";
|
||||
import { __test__, isSensitiveConfigPath } from "./schema.hints.js";
|
||||
import { FIELD_HELP } from "./schema.help.js";
|
||||
import { __test__, isPluginOwnedChannelHintPath, isSensitiveConfigPath } from "./schema.hints.js";
|
||||
import { FIELD_LABELS } from "./schema.labels.js";
|
||||
import { OpenClawSchema } from "./zod-schema.js";
|
||||
import { sensitive } from "./zod-schema.sensitive.js";
|
||||
|
||||
const { mapSensitivePaths } = __test__;
|
||||
const BUNDLED_CHANNEL_HINT_PREFIXES = [
|
||||
"channels.bluebubbles",
|
||||
"channels.discord",
|
||||
"channels.imessage",
|
||||
"channels.irc",
|
||||
"channels.msteams",
|
||||
"channels.signal",
|
||||
"channels.slack",
|
||||
"channels.telegram",
|
||||
"channels.whatsapp",
|
||||
] as const;
|
||||
|
||||
describe("isSensitiveConfigPath", () => {
|
||||
it("matches whitelist suffixes case-insensitively", () => {
|
||||
@@ -36,6 +49,21 @@ describe("isSensitiveConfigPath", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugin-owned channel hint paths", () => {
|
||||
it("keeps bundled channel help and labels out of core tables", () => {
|
||||
for (const key of [...Object.keys(FIELD_HELP), ...Object.keys(FIELD_LABELS)]) {
|
||||
if (
|
||||
!BUNDLED_CHANNEL_HINT_PREFIXES.some(
|
||||
(prefix) => key === prefix || key.startsWith(`${prefix}.`),
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
expect(isPluginOwnedChannelHintPath(key), `core still owns ${key}`).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapSensitivePaths", () => {
|
||||
it("should detect sensitive fields nested inside all structural Zod types", () => {
|
||||
const GrandSchema = z.object({
|
||||
@@ -135,8 +163,6 @@ describe("mapSensitivePaths", () => {
|
||||
|
||||
expect(hints["agents.defaults.memorySearch.remote.apiKey"]?.sensitive).toBe(true);
|
||||
expect(hints["agents.list[].memorySearch.remote.apiKey"]?.sensitive).toBe(true);
|
||||
expect(hints["channels.discord.accounts.*.token"]?.sensitive).toBe(true);
|
||||
expect(hints["channels.googlechat.serviceAccount"]?.sensitive).toBe(true);
|
||||
expect(hints["gateway.auth.token"]?.sensitive).toBe(true);
|
||||
expect(hints["models.providers.*.headers.*"]?.sensitive).toBe(true);
|
||||
expect(hints["skills.entries.*.apiKey"]?.sensitive).toBe(true);
|
||||
|
||||
@@ -87,6 +87,25 @@ const FIELD_PLACEHOLDERS: Record<string, string> = {
|
||||
"agents.list[].identity.avatar": "avatars/openclaw.png",
|
||||
};
|
||||
|
||||
const CHANNEL_NAMESPACE_PREFIX = "channels.";
|
||||
const CHANNEL_KERNEL_HINT_PREFIXES = ["channels.defaults", "channels.modelByChannel"] as const;
|
||||
|
||||
function isKernelOwnedChannelHintPath(path: string): boolean {
|
||||
if (path === "channels") {
|
||||
return true;
|
||||
}
|
||||
return CHANNEL_KERNEL_HINT_PREFIXES.some(
|
||||
(prefix) => path === prefix || path.startsWith(`${prefix}.`),
|
||||
);
|
||||
}
|
||||
|
||||
export function isPluginOwnedChannelHintPath(path: string): boolean {
|
||||
if (!path.startsWith(CHANNEL_NAMESPACE_PREFIX)) {
|
||||
return false;
|
||||
}
|
||||
return !isKernelOwnedChannelHintPath(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-sensitive field names that happen to match sensitive patterns.
|
||||
* These are explicitly excluded from redaction (plugin config) and
|
||||
@@ -140,14 +159,23 @@ export function buildBaseHints(): ConfigUiHints {
|
||||
};
|
||||
}
|
||||
for (const [path, label] of Object.entries(FIELD_LABELS)) {
|
||||
if (isPluginOwnedChannelHintPath(path)) {
|
||||
continue;
|
||||
}
|
||||
const current = hints[path];
|
||||
hints[path] = current ? { ...current, label } : { label };
|
||||
}
|
||||
for (const [path, help] of Object.entries(FIELD_HELP)) {
|
||||
if (isPluginOwnedChannelHintPath(path)) {
|
||||
continue;
|
||||
}
|
||||
const current = hints[path];
|
||||
hints[path] = current ? { ...current, help } : { help };
|
||||
}
|
||||
for (const [path, placeholder] of Object.entries(FIELD_PLACEHOLDERS)) {
|
||||
if (isPluginOwnedChannelHintPath(path)) {
|
||||
continue;
|
||||
}
|
||||
const current = hints[path];
|
||||
hints[path] = current ? { ...current, placeholder } : { placeholder };
|
||||
}
|
||||
@@ -159,15 +187,14 @@ export function applySensitiveHints(
|
||||
allowedKeys?: ReadonlySet<string>,
|
||||
): ConfigUiHints {
|
||||
const next = { ...hints };
|
||||
for (const key of Object.keys(next)) {
|
||||
if (allowedKeys && !allowedKeys.has(key)) {
|
||||
continue;
|
||||
}
|
||||
if (next[key]?.sensitive !== undefined) {
|
||||
const keys = allowedKeys ? [...allowedKeys] : Object.keys(next);
|
||||
for (const key of keys) {
|
||||
const current = next[key];
|
||||
if (current?.sensitive !== undefined) {
|
||||
continue;
|
||||
}
|
||||
if (isSensitiveConfigPath(key)) {
|
||||
next[key] = { ...next[key], sensitive: true };
|
||||
next[key] = { ...current, sensitive: true };
|
||||
}
|
||||
}
|
||||
return next;
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
export const IRC_FIELD_LABELS: Record<string, string> = {
|
||||
"channels.irc": "IRC",
|
||||
"channels.irc.dmPolicy": "IRC DM Policy",
|
||||
"channels.irc.nickserv.enabled": "IRC NickServ Enabled",
|
||||
"channels.irc.nickserv.service": "IRC NickServ Service",
|
||||
"channels.irc.nickserv.password": "IRC NickServ Password",
|
||||
"channels.irc.nickserv.passwordFile": "IRC NickServ Password File",
|
||||
"channels.irc.nickserv.register": "IRC NickServ Register",
|
||||
"channels.irc.nickserv.registerEmail": "IRC NickServ Register Email",
|
||||
};
|
||||
|
||||
export const IRC_FIELD_HELP: Record<string, string> = {
|
||||
"channels.irc.configWrites":
|
||||
"Allow IRC to write config in response to channel events/commands (default: true).",
|
||||
"channels.irc.dmPolicy":
|
||||
'Direct message access control ("pairing" recommended). "open" requires channels.irc.allowFrom=["*"].',
|
||||
"channels.irc.nickserv.enabled":
|
||||
"Enable NickServ identify/register after connect (defaults to enabled when password is configured).",
|
||||
"channels.irc.nickserv.service": "NickServ service nick (default: NickServ).",
|
||||
"channels.irc.nickserv.password": "NickServ password used for IDENTIFY/REGISTER (sensitive).",
|
||||
"channels.irc.nickserv.passwordFile": "Optional file path containing NickServ password.",
|
||||
"channels.irc.nickserv.register":
|
||||
"If true, send NickServ REGISTER on every connect. Use once for initial registration, then disable.",
|
||||
"channels.irc.nickserv.registerEmail":
|
||||
"Email used with NickServ REGISTER (required when register=true).",
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
import { MEDIA_AUDIO_FIELD_LABELS } from "./media-audio-field-metadata.js";
|
||||
import { IRC_FIELD_LABELS } from "./schema.irc.js";
|
||||
|
||||
export const FIELD_LABELS: Record<string, string> = {
|
||||
meta: "Metadata",
|
||||
@@ -715,132 +714,15 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"channels.defaults.heartbeat.showOk": "Heartbeat Show OK",
|
||||
"channels.defaults.heartbeat.showAlerts": "Heartbeat Show Alerts",
|
||||
"channels.defaults.heartbeat.useIndicator": "Heartbeat Use Indicator",
|
||||
"channels.whatsapp": "WhatsApp",
|
||||
"channels.telegram": "Telegram",
|
||||
"channels.telegram.customCommands": "Telegram Custom Commands",
|
||||
"channels.discord": "Discord",
|
||||
"channels.slack": "Slack",
|
||||
"channels.mattermost": "Mattermost",
|
||||
"channels.signal": "Signal",
|
||||
"channels.imessage": "iMessage",
|
||||
"channels.bluebubbles": "BlueBubbles",
|
||||
"channels.msteams": "MS Teams",
|
||||
"channels.modelByChannel": "Channel Model Overrides",
|
||||
...IRC_FIELD_LABELS,
|
||||
"channels.telegram.botToken": "Telegram Bot Token",
|
||||
"channels.telegram.dmPolicy": "Telegram DM Policy",
|
||||
"channels.telegram.configWrites": "Telegram Config Writes",
|
||||
"channels.telegram.commands.native": "Telegram Native Commands",
|
||||
"channels.telegram.commands.nativeSkills": "Telegram Native Skill Commands",
|
||||
"channels.telegram.streaming": "Telegram Streaming Mode",
|
||||
"channels.telegram.retry.attempts": "Telegram Retry Attempts",
|
||||
"channels.telegram.retry.minDelayMs": "Telegram Retry Min Delay (ms)",
|
||||
"channels.telegram.retry.maxDelayMs": "Telegram Retry Max Delay (ms)",
|
||||
"channels.telegram.retry.jitter": "Telegram Retry Jitter",
|
||||
"channels.telegram.network.autoSelectFamily": "Telegram autoSelectFamily",
|
||||
"channels.telegram.timeoutSeconds": "Telegram API Timeout (seconds)",
|
||||
"channels.telegram.silentErrorReplies": "Telegram Silent Error Replies",
|
||||
"channels.telegram.apiRoot": "Telegram API Root URL",
|
||||
"channels.telegram.autoTopicLabel": "Telegram Auto Topic Label",
|
||||
"channels.telegram.autoTopicLabel.enabled": "Telegram Auto Topic Label Enabled",
|
||||
"channels.telegram.autoTopicLabel.prompt": "Telegram Auto Topic Label Prompt",
|
||||
"channels.telegram.capabilities.inlineButtons": "Telegram Inline Buttons",
|
||||
"channels.telegram.execApprovals": "Telegram Exec Approvals",
|
||||
"channels.telegram.execApprovals.enabled": "Telegram Exec Approvals Enabled",
|
||||
"channels.telegram.execApprovals.approvers": "Telegram Exec Approval Approvers",
|
||||
"channels.telegram.execApprovals.agentFilter": "Telegram Exec Approval Agent Filter",
|
||||
"channels.telegram.execApprovals.sessionFilter": "Telegram Exec Approval Session Filter",
|
||||
"channels.telegram.execApprovals.target": "Telegram Exec Approval Target",
|
||||
"channels.telegram.threadBindings.enabled": "Telegram Thread Binding Enabled",
|
||||
"channels.telegram.threadBindings.idleHours": "Telegram Thread Binding Idle Timeout (hours)",
|
||||
"channels.telegram.threadBindings.maxAgeHours": "Telegram Thread Binding Max Age (hours)",
|
||||
"channels.telegram.threadBindings.spawnSubagentSessions": "Telegram Thread-Bound Subagent Spawn",
|
||||
"channels.telegram.threadBindings.spawnAcpSessions": "Telegram Thread-Bound ACP Spawn",
|
||||
"channels.whatsapp.dmPolicy": "WhatsApp DM Policy",
|
||||
"channels.whatsapp.selfChatMode": "WhatsApp Self-Phone Mode",
|
||||
"channels.whatsapp.debounceMs": "WhatsApp Message Debounce (ms)",
|
||||
"channels.whatsapp.configWrites": "WhatsApp Config Writes",
|
||||
"channels.signal.dmPolicy": "Signal DM Policy",
|
||||
"channels.signal.configWrites": "Signal Config Writes",
|
||||
"channels.imessage.dmPolicy": "iMessage DM Policy",
|
||||
"channels.imessage.configWrites": "iMessage Config Writes",
|
||||
"channels.bluebubbles.dmPolicy": "BlueBubbles DM Policy",
|
||||
"channels.msteams.configWrites": "MS Teams Config Writes",
|
||||
"channels.irc.configWrites": "IRC Config Writes",
|
||||
"channels.discord.dmPolicy": "Discord DM Policy",
|
||||
"channels.discord.dm.policy": "Discord DM Policy",
|
||||
"channels.discord.configWrites": "Discord Config Writes",
|
||||
"channels.discord.proxy": "Discord Proxy URL",
|
||||
"channels.discord.commands.native": "Discord Native Commands",
|
||||
"channels.discord.commands.nativeSkills": "Discord Native Skill Commands",
|
||||
"channels.discord.streaming": "Discord Streaming Mode",
|
||||
"channels.discord.streamMode": "Discord Stream Mode (Legacy)",
|
||||
"channels.discord.draftChunk.minChars": "Discord Draft Chunk Min Chars",
|
||||
"channels.discord.draftChunk.maxChars": "Discord Draft Chunk Max Chars",
|
||||
"channels.discord.draftChunk.breakPreference": "Discord Draft Chunk Break Preference",
|
||||
"channels.discord.retry.attempts": "Discord Retry Attempts",
|
||||
"channels.discord.retry.minDelayMs": "Discord Retry Min Delay (ms)",
|
||||
"channels.discord.retry.maxDelayMs": "Discord Retry Max Delay (ms)",
|
||||
"channels.discord.retry.jitter": "Discord Retry Jitter",
|
||||
"channels.discord.maxLinesPerMessage": "Discord Max Lines Per Message",
|
||||
"channels.discord.inboundWorker.runTimeoutMs": "Discord Inbound Worker Timeout (ms)",
|
||||
"channels.discord.eventQueue.listenerTimeout": "Discord EventQueue Listener Timeout (ms)",
|
||||
"channels.discord.eventQueue.maxQueueSize": "Discord EventQueue Max Queue Size",
|
||||
"channels.discord.eventQueue.maxConcurrency": "Discord EventQueue Max Concurrency",
|
||||
"channels.discord.threadBindings.enabled": "Discord Thread Binding Enabled",
|
||||
"channels.discord.threadBindings.idleHours": "Discord Thread Binding Idle Timeout (hours)",
|
||||
"channels.discord.threadBindings.maxAgeHours": "Discord Thread Binding Max Age (hours)",
|
||||
"channels.discord.threadBindings.spawnSubagentSessions": "Discord Thread-Bound Subagent Spawn",
|
||||
"channels.discord.threadBindings.spawnAcpSessions": "Discord Thread-Bound ACP Spawn",
|
||||
"channels.discord.ui.components.accentColor": "Discord Component Accent Color",
|
||||
"channels.discord.intents.presence": "Discord Presence Intent",
|
||||
"channels.discord.intents.guildMembers": "Discord Guild Members Intent",
|
||||
"channels.discord.voice.enabled": "Discord Voice Enabled",
|
||||
"channels.discord.voice.autoJoin": "Discord Voice Auto-Join",
|
||||
"channels.discord.voice.daveEncryption": "Discord Voice DAVE Encryption",
|
||||
"channels.discord.voice.decryptionFailureTolerance": "Discord Voice Decrypt Failure Tolerance",
|
||||
"channels.discord.voice.tts": "Discord Voice Text-to-Speech",
|
||||
"channels.discord.pluralkit.enabled": "Discord PluralKit Enabled",
|
||||
"channels.discord.pluralkit.token": "Discord PluralKit Token",
|
||||
"channels.discord.activity": "Discord Presence Activity",
|
||||
"channels.discord.status": "Discord Presence Status",
|
||||
"channels.discord.autoPresence.enabled": "Discord Auto Presence Enabled",
|
||||
"channels.discord.autoPresence.intervalMs": "Discord Auto Presence Check Interval (ms)",
|
||||
"channels.discord.autoPresence.minUpdateIntervalMs":
|
||||
"Discord Auto Presence Min Update Interval (ms)",
|
||||
"channels.discord.autoPresence.healthyText": "Discord Auto Presence Healthy Text",
|
||||
"channels.discord.autoPresence.degradedText": "Discord Auto Presence Degraded Text",
|
||||
"channels.discord.autoPresence.exhaustedText": "Discord Auto Presence Exhausted Text",
|
||||
"channels.discord.activityType": "Discord Presence Activity Type",
|
||||
"channels.discord.activityUrl": "Discord Presence Activity URL",
|
||||
"channels.slack.dm.policy": "Slack DM Policy",
|
||||
"channels.slack.dmPolicy": "Slack DM Policy",
|
||||
"channels.slack.configWrites": "Slack Config Writes",
|
||||
"channels.slack.commands.native": "Slack Native Commands",
|
||||
"channels.slack.commands.nativeSkills": "Slack Native Skill Commands",
|
||||
"channels.slack.allowBots": "Slack Allow Bot Messages",
|
||||
"channels.discord.allowBots": "Discord Allow Bot Messages",
|
||||
"channels.matrix.allowBots": "Matrix Allow Bot Messages",
|
||||
"channels.discord.token": "Discord Bot Token",
|
||||
"channels.slack.botToken": "Slack Bot Token",
|
||||
"channels.slack.appToken": "Slack App Token",
|
||||
"channels.slack.userToken": "Slack User Token",
|
||||
"channels.slack.userTokenReadOnly": "Slack User Token Read Only",
|
||||
"channels.slack.capabilities.interactiveReplies": "Slack Interactive Replies",
|
||||
"channels.slack.streaming": "Slack Streaming Mode",
|
||||
"channels.slack.nativeStreaming": "Slack Native Streaming",
|
||||
"channels.slack.streamMode": "Slack Stream Mode (Legacy)",
|
||||
"channels.slack.thread.historyScope": "Slack Thread History Scope",
|
||||
"channels.slack.thread.inheritParent": "Slack Thread Parent Inheritance",
|
||||
"channels.slack.thread.initialHistoryLimit": "Slack Thread Initial History Limit",
|
||||
"channels.mattermost.botToken": "Mattermost Bot Token",
|
||||
"channels.mattermost.baseUrl": "Mattermost Base URL",
|
||||
"channels.mattermost.configWrites": "Mattermost Config Writes",
|
||||
"channels.mattermost.chatmode": "Mattermost Chat Mode",
|
||||
"channels.mattermost.oncharPrefixes": "Mattermost Onchar Prefixes",
|
||||
"channels.mattermost.requireMention": "Mattermost Require Mention",
|
||||
"channels.signal.account": "Signal Account",
|
||||
"channels.imessage.cliPath": "iMessage CLI Path",
|
||||
"agents.list[].skills": "Agent Skill Filter",
|
||||
"agents.list[].identity.avatar": "Agent Avatar",
|
||||
"agents.list[].heartbeat.suppressToolErrorWarnings":
|
||||
|
||||
@@ -99,7 +99,7 @@ describe("config schema", () => {
|
||||
expect(schema.properties?.$schema).toBeUndefined();
|
||||
expect(res.uiHints.gateway?.label).toBe("Gateway");
|
||||
expect(res.uiHints["gateway.auth.token"]?.sensitive).toBe(true);
|
||||
expect(res.uiHints["channels.discord.threadBindings.spawnAcpSessions"]?.label).toBeTruthy();
|
||||
expect(res.uiHints["channels.defaults.groupPolicy"]?.label).toBeTruthy();
|
||||
expect(res.version).toBeTruthy();
|
||||
expect(res.generatedAt).toBeTruthy();
|
||||
});
|
||||
@@ -142,6 +142,8 @@ describe("config schema", () => {
|
||||
const channelSchema = channelsProps?.matrix as Record<string, unknown> | undefined;
|
||||
const channelProps = channelSchema?.properties as Record<string, unknown> | undefined;
|
||||
expect(channelProps?.accessToken).toBeTruthy();
|
||||
expect(res.uiHints["channels.matrix"]?.label).toBe("Matrix");
|
||||
expect(res.uiHints["channels.matrix.accessToken"]?.sensitive).toBe(true);
|
||||
});
|
||||
|
||||
it("looks up plugin config paths for slash-delimited plugin ids", () => {
|
||||
|
||||
@@ -141,21 +141,61 @@ function collectExtensionHintKeys(
|
||||
plugins: PluginUiMetadata[],
|
||||
channels: ChannelUiMetadata[],
|
||||
): Set<string> {
|
||||
const pluginPrefixes = plugins
|
||||
.map((plugin) => plugin.id.trim())
|
||||
.filter(Boolean)
|
||||
.map((id) => `plugins.entries.${id}`);
|
||||
const channelPrefixes = channels
|
||||
.map((channel) => channel.id.trim())
|
||||
.filter(Boolean)
|
||||
.map((id) => `channels.${id}`);
|
||||
const prefixes = [...pluginPrefixes, ...channelPrefixes];
|
||||
const keys = new Set<string>();
|
||||
const collectPrefixedHintKeys = (prefix: string) => {
|
||||
for (const key of Object.keys(hints)) {
|
||||
if (key === prefix || key.startsWith(`${prefix}.`)) {
|
||||
keys.add(key);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return new Set(
|
||||
Object.keys(hints).filter((key) =>
|
||||
prefixes.some((prefix) => key === prefix || key.startsWith(`${prefix}.`)),
|
||||
),
|
||||
);
|
||||
const collectSchemaKeys = (schema: unknown, basePath: string) => {
|
||||
const node = asJsonSchemaObject(schema);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
keys.add(basePath);
|
||||
for (const [propertyKey, propertySchema] of Object.entries(node.properties ?? {})) {
|
||||
collectSchemaKeys(propertySchema, `${basePath}.${propertyKey}`);
|
||||
}
|
||||
if (node.additionalProperties && typeof node.additionalProperties === "object") {
|
||||
collectSchemaKeys(node.additionalProperties, `${basePath}.*`);
|
||||
}
|
||||
if (Array.isArray(node.items)) {
|
||||
for (const item of node.items) {
|
||||
if (item && typeof item === "object") {
|
||||
collectSchemaKeys(item, `${basePath}[]`);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (node.items && typeof node.items === "object") {
|
||||
collectSchemaKeys(node.items, `${basePath}[]`);
|
||||
}
|
||||
};
|
||||
|
||||
for (const plugin of plugins) {
|
||||
const id = plugin.id.trim();
|
||||
if (!id) {
|
||||
continue;
|
||||
}
|
||||
const prefix = `plugins.entries.${id}`;
|
||||
collectPrefixedHintKeys(prefix);
|
||||
collectSchemaKeys(plugin.configSchema, `${prefix}.config`);
|
||||
}
|
||||
|
||||
for (const channel of channels) {
|
||||
const id = channel.id.trim();
|
||||
if (!id) {
|
||||
continue;
|
||||
}
|
||||
const prefix = `channels.${id}`;
|
||||
collectPrefixedHintKeys(prefix);
|
||||
collectSchemaKeys(channel.configSchema, prefix);
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
function applyPluginHints(hints: ConfigUiHints, plugins: PluginUiMetadata[]): ConfigUiHints {
|
||||
|
||||
57
src/config/validation.channel-metadata.test.ts
Normal file
57
src/config/validation.channel-metadata.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mockLoadPluginManifestRegistry = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../plugins/manifest-registry.js", () => ({
|
||||
loadPluginManifestRegistry: (...args: unknown[]) => mockLoadPluginManifestRegistry(...args),
|
||||
}));
|
||||
|
||||
describe("validateConfigObjectRawWithPlugins channel metadata", () => {
|
||||
it("applies bundled channel defaults from plugin-owned schema metadata", async () => {
|
||||
mockLoadPluginManifestRegistry.mockReturnValue({
|
||||
diagnostics: [],
|
||||
plugins: [
|
||||
{
|
||||
id: "telegram",
|
||||
origin: "bundled",
|
||||
channels: ["telegram"],
|
||||
channelCatalogMeta: {
|
||||
id: "telegram",
|
||||
label: "Telegram",
|
||||
blurb: "Telegram channel",
|
||||
},
|
||||
channelConfigs: {
|
||||
telegram: {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
dmPolicy: {
|
||||
type: "string",
|
||||
enum: ["pairing", "allowlist"],
|
||||
default: "pairing",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
uiHints: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { validateConfigObjectRawWithPlugins } = await import("./validation.js");
|
||||
const result = validateConfigObjectRawWithPlugins({
|
||||
channels: {
|
||||
telegram: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.config.channels?.telegram).toEqual(
|
||||
expect.objectContaining({ dmPolicy: "pairing" }),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,7 @@ import { isCanonicalDottedDecimalIPv4, isLoopbackIpAddress } from "../shared/net
|
||||
import { isRecord } from "../utils.js";
|
||||
import { findDuplicateAgentDirs, formatDuplicateAgentDirError } from "./agent-dirs.js";
|
||||
import { appendAllowedValuesHint, summarizeAllowedValues } from "./allowed-values.js";
|
||||
import { collectChannelSchemaMetadata } from "./channel-config-metadata.js";
|
||||
import { applyAgentDefaults, applyModelDefaults, applySessionDefaults } from "./defaults.js";
|
||||
import {
|
||||
listLegacyWebSearchConfigPaths,
|
||||
@@ -253,7 +254,8 @@ export function validateConfigObjectRaw(
|
||||
issues: validated.error.issues.map((issue) => mapZodIssueToConfigIssue(issue)),
|
||||
};
|
||||
}
|
||||
const duplicates = findDuplicateAgentDirs(validated.data as OpenClawConfig);
|
||||
const validatedConfig = validated.data as OpenClawConfig;
|
||||
const duplicates = findDuplicateAgentDirs(validatedConfig);
|
||||
if (duplicates.length > 0) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -265,17 +267,17 @@ export function validateConfigObjectRaw(
|
||||
],
|
||||
};
|
||||
}
|
||||
const avatarIssues = validateIdentityAvatar(validated.data as OpenClawConfig);
|
||||
const avatarIssues = validateIdentityAvatar(validatedConfig);
|
||||
if (avatarIssues.length > 0) {
|
||||
return { ok: false, issues: avatarIssues };
|
||||
}
|
||||
const gatewayTailscaleBindIssues = validateGatewayTailscaleBind(validated.data as OpenClawConfig);
|
||||
const gatewayTailscaleBindIssues = validateGatewayTailscaleBind(validatedConfig);
|
||||
if (gatewayTailscaleBindIssues.length > 0) {
|
||||
return { ok: false, issues: gatewayTailscaleBindIssues };
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
config: validated.data as OpenClawConfig,
|
||||
config: validatedConfig,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -350,6 +352,12 @@ function validateConfigObjectWithPluginsBase(
|
||||
registry: ReturnType<typeof loadPluginManifestRegistry>;
|
||||
knownIds?: Set<string>;
|
||||
normalizedPlugins?: ReturnType<typeof normalizePluginsConfig>;
|
||||
channelSchemas?: Map<
|
||||
string,
|
||||
{
|
||||
schema?: Record<string, unknown>;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
let registryInfo: RegistryInfo | null = null;
|
||||
@@ -441,6 +449,67 @@ function validateConfigObjectWithPluginsBase(
|
||||
return info.normalizedPlugins;
|
||||
};
|
||||
|
||||
const ensureChannelSchemas = (): Map<
|
||||
string,
|
||||
{
|
||||
schema?: Record<string, unknown>;
|
||||
}
|
||||
> => {
|
||||
const info = ensureRegistry();
|
||||
if (!info.channelSchemas) {
|
||||
info.channelSchemas = new Map(
|
||||
collectChannelSchemaMetadata(info.registry).map(
|
||||
(entry) => [entry.id, { schema: entry.configSchema }] as const,
|
||||
),
|
||||
);
|
||||
}
|
||||
return info.channelSchemas;
|
||||
};
|
||||
|
||||
let mutatedConfig = config;
|
||||
let channelsCloned = false;
|
||||
let pluginsCloned = false;
|
||||
let pluginEntriesCloned = false;
|
||||
|
||||
const replaceChannelConfig = (channelId: string, nextValue: unknown) => {
|
||||
if (!channelsCloned) {
|
||||
mutatedConfig = {
|
||||
...mutatedConfig,
|
||||
channels: {
|
||||
...mutatedConfig.channels,
|
||||
},
|
||||
};
|
||||
channelsCloned = true;
|
||||
}
|
||||
(mutatedConfig.channels as Record<string, unknown>)[channelId] = nextValue;
|
||||
};
|
||||
|
||||
const replacePluginEntryConfig = (pluginId: string, nextValue: Record<string, unknown>) => {
|
||||
if (!pluginsCloned) {
|
||||
mutatedConfig = {
|
||||
...mutatedConfig,
|
||||
plugins: {
|
||||
...mutatedConfig.plugins,
|
||||
},
|
||||
};
|
||||
pluginsCloned = true;
|
||||
}
|
||||
if (!pluginEntriesCloned) {
|
||||
mutatedConfig.plugins = {
|
||||
...mutatedConfig.plugins,
|
||||
entries: {
|
||||
...mutatedConfig.plugins?.entries,
|
||||
},
|
||||
};
|
||||
pluginEntriesCloned = true;
|
||||
}
|
||||
const currentEntry = mutatedConfig.plugins?.entries?.[pluginId];
|
||||
mutatedConfig.plugins!.entries![pluginId] = {
|
||||
...currentEntry,
|
||||
config: nextValue,
|
||||
};
|
||||
};
|
||||
|
||||
const allowedChannels = new Set<string>(["defaults", "modelByChannel", ...CHANNEL_IDS]);
|
||||
|
||||
if (config.channels && isRecord(config.channels)) {
|
||||
@@ -462,7 +531,32 @@ function validateConfigObjectWithPluginsBase(
|
||||
path: `channels.${trimmed}`,
|
||||
message: `unknown channel id: ${trimmed}`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const channelSchema = ensureChannelSchemas().get(trimmed)?.schema;
|
||||
if (!channelSchema) {
|
||||
continue;
|
||||
}
|
||||
const result = validateJsonSchemaValue({
|
||||
schema: channelSchema,
|
||||
cacheKey: `channel:${trimmed}`,
|
||||
value: config.channels[trimmed],
|
||||
applyDefaults: true,
|
||||
});
|
||||
if (!result.ok) {
|
||||
for (const error of result.errors) {
|
||||
issues.push({
|
||||
path:
|
||||
error.path === "<root>" ? `channels.${trimmed}` : `channels.${trimmed}.${error.path}`,
|
||||
message: `invalid config: ${error.message}`,
|
||||
allowedValues: error.allowedValues,
|
||||
allowedValuesHiddenCount: error.allowedValuesHiddenCount,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
replaceChannelConfig(trimmed, result.value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,7 +612,7 @@ function validateConfigObjectWithPluginsBase(
|
||||
if (issues.length > 0) {
|
||||
return { ok: false, issues, warnings };
|
||||
}
|
||||
return { ok: true, config, warnings };
|
||||
return { ok: true, config: mutatedConfig, warnings };
|
||||
}
|
||||
|
||||
const { registry } = ensureRegistry();
|
||||
@@ -638,6 +732,7 @@ function validateConfigObjectWithPluginsBase(
|
||||
schema: record.configSchema,
|
||||
cacheKey: record.schemaCacheKey ?? record.manifestPath ?? pluginId,
|
||||
value: entry?.config ?? {},
|
||||
applyDefaults: true,
|
||||
});
|
||||
if (!res.ok) {
|
||||
for (const error of res.errors) {
|
||||
@@ -648,6 +743,8 @@ function validateConfigObjectWithPluginsBase(
|
||||
allowedValuesHiddenCount: error.allowedValuesHiddenCount,
|
||||
});
|
||||
}
|
||||
} else if (entry || entryHasConfig) {
|
||||
replacePluginEntryConfig(pluginId, res.value as Record<string, unknown>);
|
||||
}
|
||||
} else if (record.format === "bundle") {
|
||||
// Compatible bundles currently expose no native OpenClaw config schema.
|
||||
@@ -672,5 +769,5 @@ function validateConfigObjectWithPluginsBase(
|
||||
return { ok: false, issues, warnings };
|
||||
}
|
||||
|
||||
return { ok: true, config, warnings };
|
||||
return { ok: true, config: mutatedConfig, warnings };
|
||||
}
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
import { z } from "zod";
|
||||
import { getBundledChannelRuntimeMap } from "./bundled-channel-config-runtime.js";
|
||||
import type { ChannelsConfig } from "./types.channels.js";
|
||||
import { ChannelHeartbeatVisibilitySchema } from "./zod-schema.channels.js";
|
||||
import { GroupPolicySchema } from "./zod-schema.core.js";
|
||||
import {
|
||||
BlueBubblesConfigSchema,
|
||||
DiscordConfigSchema,
|
||||
GoogleChatConfigSchema,
|
||||
IMessageConfigSchema,
|
||||
IrcConfigSchema,
|
||||
MSTeamsConfigSchema,
|
||||
SignalConfigSchema,
|
||||
SlackConfigSchema,
|
||||
TelegramConfigSchema,
|
||||
} from "./zod-schema.providers-core.js";
|
||||
import { WhatsAppConfigSchema } from "./zod-schema.providers-whatsapp.js";
|
||||
|
||||
export * from "./zod-schema.providers-core.js";
|
||||
export * from "./zod-schema.providers-whatsapp.js";
|
||||
@@ -22,7 +12,70 @@ const ChannelModelByChannelSchema = z
|
||||
.record(z.string(), z.record(z.string(), z.string()))
|
||||
.optional();
|
||||
|
||||
export const ChannelsSchema = z
|
||||
function addLegacyChannelAcpBindingIssues(
|
||||
value: unknown,
|
||||
ctx: z.RefinementCtx,
|
||||
path: Array<string | number> = [],
|
||||
) {
|
||||
if (!value || typeof value !== "object") {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((entry, index) => addLegacyChannelAcpBindingIssues(entry, ctx, [...path, index]));
|
||||
return;
|
||||
}
|
||||
|
||||
const record = value as Record<string, unknown>;
|
||||
const bindings = record.bindings;
|
||||
if (bindings && typeof bindings === "object" && !Array.isArray(bindings)) {
|
||||
const acp = (bindings as Record<string, unknown>).acp;
|
||||
if (acp && typeof acp === "object") {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: [...path, "bindings", "acp"],
|
||||
message:
|
||||
"Legacy channel-local ACP bindings were removed; use top-level bindings[] entries.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, entry] of Object.entries(record)) {
|
||||
addLegacyChannelAcpBindingIssues(entry, ctx, [...path, key]);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeBundledChannelConfigs(
|
||||
value: ChannelsConfig | undefined,
|
||||
ctx: z.RefinementCtx,
|
||||
): ChannelsConfig | undefined {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
let next: ChannelsConfig | undefined;
|
||||
for (const [channelId, runtimeSchema] of getBundledChannelRuntimeMap()) {
|
||||
if (!Object.prototype.hasOwnProperty.call(value, channelId)) {
|
||||
continue;
|
||||
}
|
||||
const parsed = runtimeSchema.safeParse(value[channelId]);
|
||||
if (!parsed.success) {
|
||||
for (const issue of parsed.issues) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: issue.message ?? `Invalid channels.${channelId} config.`,
|
||||
path: [channelId, ...(Array.isArray(issue.path) ? issue.path : [])],
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
next ??= { ...value };
|
||||
next[channelId] = parsed.data as ChannelsConfig[string];
|
||||
}
|
||||
|
||||
return next ?? value;
|
||||
}
|
||||
|
||||
export const ChannelsSchema: z.ZodType<ChannelsConfig | undefined> = z
|
||||
.object({
|
||||
defaults: z
|
||||
.object({
|
||||
@@ -32,16 +85,10 @@ export const ChannelsSchema = z
|
||||
.strict()
|
||||
.optional(),
|
||||
modelByChannel: ChannelModelByChannelSchema,
|
||||
whatsapp: WhatsAppConfigSchema.optional(),
|
||||
telegram: TelegramConfigSchema.optional(),
|
||||
discord: DiscordConfigSchema.optional(),
|
||||
irc: IrcConfigSchema.optional(),
|
||||
googlechat: GoogleChatConfigSchema.optional(),
|
||||
slack: SlackConfigSchema.optional(),
|
||||
signal: SignalConfigSchema.optional(),
|
||||
imessage: IMessageConfigSchema.optional(),
|
||||
bluebubbles: BlueBubblesConfigSchema.optional(),
|
||||
msteams: MSTeamsConfigSchema.optional(),
|
||||
})
|
||||
.passthrough() // Allow extension channel configs (nostr, matrix, zalo, etc.)
|
||||
.optional();
|
||||
.superRefine((value, ctx) => {
|
||||
addLegacyChannelAcpBindingIssues(value, ctx);
|
||||
})
|
||||
.transform((value, ctx) => normalizeBundledChannelConfigs(value, ctx))
|
||||
.optional() as z.ZodType<ChannelsConfig | undefined>;
|
||||
|
||||
@@ -37,6 +37,7 @@ const EXTRA_GUARDED_EXTENSION_PUBLIC_SURFACE_BASENAMES = assertUniqueValues(
|
||||
"allow-from.js",
|
||||
"api.js",
|
||||
"auth-presence.js",
|
||||
"channel-config-api.js",
|
||||
"index.js",
|
||||
"login-qr-api.js",
|
||||
"onboard.js",
|
||||
|
||||
@@ -6,8 +6,22 @@ export {
|
||||
buildNestedDmConfigSchema,
|
||||
} from "../channels/plugins/config-schema.js";
|
||||
export {
|
||||
BlockStreamingCoalesceSchema,
|
||||
DmConfigSchema,
|
||||
DmPolicySchema,
|
||||
GroupPolicySchema,
|
||||
MarkdownConfigSchema,
|
||||
ReplyRuntimeConfigSchemaShape,
|
||||
requireOpenAllowFrom,
|
||||
} from "../config/zod-schema.core.js";
|
||||
export { ToolPolicySchema } from "../config/zod-schema.agent-runtime.js";
|
||||
export {
|
||||
DiscordConfigSchema,
|
||||
GoogleChatConfigSchema,
|
||||
IMessageConfigSchema,
|
||||
MSTeamsConfigSchema,
|
||||
SignalConfigSchema,
|
||||
SlackConfigSchema,
|
||||
TelegramConfigSchema,
|
||||
} from "../config/zod-schema.providers-core.js";
|
||||
export { WhatsAppConfigSchema } from "../config/zod-schema.providers-whatsapp.js";
|
||||
|
||||
@@ -80,7 +80,7 @@ export type {
|
||||
UsageWindow,
|
||||
} from "../infra/provider-usage.types.js";
|
||||
export type { ChannelMessageActionContext } from "../channels/plugins/types.js";
|
||||
export type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
|
||||
export type { ChannelConfigUiHint, ChannelPlugin } from "../channels/plugins/types.plugin.js";
|
||||
export type { PluginRuntime } from "../plugins/runtime/types.js";
|
||||
|
||||
export { emptyPluginConfigSchema, definePluginEntry } from "./plugin-entry.js";
|
||||
|
||||
@@ -19,7 +19,11 @@ export type {
|
||||
ChannelConfiguredBindingMatch,
|
||||
ChannelConfiguredBindingProvider,
|
||||
} from "../channels/plugins/types.adapters.js";
|
||||
export type { ChannelConfigSchema, ChannelPlugin } from "../channels/plugins/types.plugin.js";
|
||||
export type {
|
||||
ChannelConfigSchema,
|
||||
ChannelConfigUiHint,
|
||||
ChannelPlugin,
|
||||
} from "../channels/plugins/types.plugin.js";
|
||||
export type { ChannelSetupAdapter, ChannelSetupInput } from "../channels/plugins/types.js";
|
||||
export type {
|
||||
ConfiguredBindingConversation,
|
||||
|
||||
@@ -250,9 +250,10 @@ function validatePluginConfig(params: {
|
||||
schema,
|
||||
cacheKey,
|
||||
value: params.value ?? {},
|
||||
applyDefaults: true,
|
||||
});
|
||||
if (result.ok) {
|
||||
return { ok: true, value: params.value as Record<string, unknown> | undefined };
|
||||
return { ok: true, value: result.value as Record<string, unknown> | undefined };
|
||||
}
|
||||
return { ok: false, errors: result.errors.map((error) => error.text) };
|
||||
}
|
||||
|
||||
@@ -284,6 +284,81 @@ describe("loadPluginManifestRegistry", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves channel config metadata from plugin manifests", () => {
|
||||
const dir = makeTempDir();
|
||||
writeManifest(dir, {
|
||||
id: "matrix",
|
||||
channels: ["matrix"],
|
||||
configSchema: { type: "object" },
|
||||
channelConfigs: {
|
||||
matrix: {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
homeserver: { type: "string" },
|
||||
},
|
||||
},
|
||||
uiHints: {
|
||||
homeserver: {
|
||||
label: "Homeserver",
|
||||
},
|
||||
},
|
||||
label: "Matrix",
|
||||
description: "Matrix config",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const registry = loadRegistry([
|
||||
createPluginCandidate({
|
||||
idHint: "matrix",
|
||||
rootDir: dir,
|
||||
origin: "workspace",
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(registry.plugins[0]?.channelConfigs).toEqual({
|
||||
matrix: {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
homeserver: { type: "string" },
|
||||
},
|
||||
},
|
||||
uiHints: {
|
||||
homeserver: {
|
||||
label: "Homeserver",
|
||||
},
|
||||
},
|
||||
label: "Matrix",
|
||||
description: "Matrix config",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("hydrates bundled channel config metadata onto manifest records", () => {
|
||||
const dir = makeTempDir();
|
||||
writeManifest(dir, {
|
||||
id: "telegram",
|
||||
channels: ["telegram"],
|
||||
configSchema: { type: "object" },
|
||||
});
|
||||
|
||||
const registry = loadSingleCandidateRegistry({
|
||||
idHint: "telegram",
|
||||
rootDir: dir,
|
||||
origin: "bundled",
|
||||
});
|
||||
|
||||
expect(registry.plugins[0]?.channelConfigs?.telegram).toEqual(
|
||||
expect.objectContaining({
|
||||
schema: expect.objectContaining({
|
||||
type: "object",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("skips plugins whose minHostVersion is newer than the current host", () => {
|
||||
const dir = makeTempDir();
|
||||
writeManifest(dir, { id: "synology-chat", configSchema: { type: "object" } });
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { BUNDLED_CHANNEL_CONFIG_METADATA } from "../config/bundled-channel-config-metadata.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { resolveRuntimeServiceVersion } from "../version.js";
|
||||
@@ -9,6 +10,7 @@ import { discoverOpenClawPlugins, type PluginCandidate } from "./discovery.js";
|
||||
import {
|
||||
loadPluginManifest,
|
||||
type PluginManifest,
|
||||
type PluginManifestChannelConfig,
|
||||
type PluginManifestContracts,
|
||||
} from "./manifest.js";
|
||||
import { checkMinHostVersion } from "./min-host-version.js";
|
||||
@@ -69,8 +71,11 @@ export type PluginManifestRecord = {
|
||||
configSchema?: Record<string, unknown>;
|
||||
configUiHints?: Record<string, PluginConfigUiHint>;
|
||||
contracts?: PluginManifestContracts;
|
||||
channelConfigs?: Record<string, PluginManifestChannelConfig>;
|
||||
channelCatalogMeta?: {
|
||||
id: string;
|
||||
label?: string;
|
||||
blurb?: string;
|
||||
preferOver?: string[];
|
||||
};
|
||||
};
|
||||
@@ -167,6 +172,7 @@ function buildRecord(params: {
|
||||
schemaCacheKey?: string;
|
||||
configSchema?: Record<string, unknown>;
|
||||
}): PluginManifestRecord {
|
||||
const bundledChannelConfigs = resolveBundledChannelConfigs(params.manifest.id);
|
||||
return {
|
||||
id: params.manifest.id,
|
||||
name: normalizeManifestLabel(params.manifest.name) ?? params.candidate.packageName,
|
||||
@@ -201,10 +207,17 @@ function buildRecord(params: {
|
||||
configSchema: params.configSchema,
|
||||
configUiHints: params.manifest.uiHints,
|
||||
contracts: params.manifest.contracts,
|
||||
channelConfigs: mergeChannelConfigs(bundledChannelConfigs, params.manifest.channelConfigs),
|
||||
...(params.candidate.packageManifest?.channel?.id
|
||||
? {
|
||||
channelCatalogMeta: {
|
||||
id: params.candidate.packageManifest.channel.id,
|
||||
...(typeof params.candidate.packageManifest.channel.label === "string"
|
||||
? { label: params.candidate.packageManifest.channel.label }
|
||||
: {}),
|
||||
...(typeof params.candidate.packageManifest.channel.blurb === "string"
|
||||
? { blurb: params.candidate.packageManifest.channel.blurb }
|
||||
: {}),
|
||||
...(params.candidate.packageManifest.channel.preferOver
|
||||
? { preferOver: params.candidate.packageManifest.channel.preferOver }
|
||||
: {}),
|
||||
@@ -214,6 +227,40 @@ function buildRecord(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveBundledChannelConfigs(
|
||||
pluginId: string,
|
||||
): Record<string, PluginManifestChannelConfig> | undefined {
|
||||
const entries = BUNDLED_CHANNEL_CONFIG_METADATA.filter((entry) => entry.pluginId === pluginId);
|
||||
if (entries.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
entries.map((entry) => [
|
||||
entry.channelId,
|
||||
{
|
||||
schema: entry.schema,
|
||||
...(entry.uiHints ? { uiHints: entry.uiHints } : {}),
|
||||
...(entry.label ? { label: entry.label } : {}),
|
||||
...(entry.description ? { description: entry.description } : {}),
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
function mergeChannelConfigs(
|
||||
generated: Record<string, PluginManifestChannelConfig> | undefined,
|
||||
manifest: Record<string, PluginManifestChannelConfig> | undefined,
|
||||
): Record<string, PluginManifestChannelConfig> | undefined {
|
||||
if (!generated) {
|
||||
return manifest;
|
||||
}
|
||||
if (!manifest) {
|
||||
return generated;
|
||||
}
|
||||
return { ...generated, ...manifest };
|
||||
}
|
||||
|
||||
function buildBundleRecord(params: {
|
||||
manifest: {
|
||||
id: string;
|
||||
@@ -253,6 +300,7 @@ function buildBundleRecord(params: {
|
||||
schemaCacheKey: undefined,
|
||||
configSchema: undefined,
|
||||
configUiHints: undefined,
|
||||
channelConfigs: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,13 @@ import type { PluginConfigUiHint, PluginKind } from "./types.js";
|
||||
export const PLUGIN_MANIFEST_FILENAME = "openclaw.plugin.json";
|
||||
export const PLUGIN_MANIFEST_FILENAMES = [PLUGIN_MANIFEST_FILENAME] as const;
|
||||
|
||||
export type PluginManifestChannelConfig = {
|
||||
schema: Record<string, unknown>;
|
||||
uiHints?: Record<string, PluginConfigUiHint>;
|
||||
label?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type PluginManifest = {
|
||||
id: string;
|
||||
configSchema: Record<string, unknown>;
|
||||
@@ -37,6 +44,7 @@ export type PluginManifest = {
|
||||
* compat wiring, and contract coverage without importing plugin runtime.
|
||||
*/
|
||||
contracts?: PluginManifestContracts;
|
||||
channelConfigs?: Record<string, PluginManifestChannelConfig>;
|
||||
};
|
||||
|
||||
export type PluginManifestContracts = {
|
||||
@@ -179,6 +187,37 @@ function normalizeProviderAuthChoices(
|
||||
return normalized.length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
function normalizeChannelConfigs(
|
||||
value: unknown,
|
||||
): Record<string, PluginManifestChannelConfig> | undefined {
|
||||
if (!isRecord(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized: Record<string, PluginManifestChannelConfig> = {};
|
||||
for (const [key, rawEntry] of Object.entries(value)) {
|
||||
const channelId = typeof key === "string" ? key.trim() : "";
|
||||
if (!channelId || !isRecord(rawEntry)) {
|
||||
continue;
|
||||
}
|
||||
const schema = isRecord(rawEntry.schema) ? rawEntry.schema : null;
|
||||
if (!schema) {
|
||||
continue;
|
||||
}
|
||||
const uiHints = isRecord(rawEntry.uiHints)
|
||||
? (rawEntry.uiHints as Record<string, PluginConfigUiHint>)
|
||||
: undefined;
|
||||
const label = typeof rawEntry.label === "string" ? rawEntry.label.trim() : "";
|
||||
const description = typeof rawEntry.description === "string" ? rawEntry.description.trim() : "";
|
||||
normalized[channelId] = {
|
||||
schema,
|
||||
...(uiHints ? { uiHints } : {}),
|
||||
...(label ? { label } : {}),
|
||||
...(description ? { description } : {}),
|
||||
};
|
||||
}
|
||||
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
export function resolvePluginManifestPath(rootDir: string): string {
|
||||
for (const filename of PLUGIN_MANIFEST_FILENAMES) {
|
||||
const candidate = path.join(rootDir, filename);
|
||||
@@ -253,6 +292,7 @@ export function loadPluginManifest(
|
||||
const providerAuthChoices = normalizeProviderAuthChoices(raw.providerAuthChoices);
|
||||
const skills = normalizeStringList(raw.skills);
|
||||
const contracts = normalizeManifestContracts(raw.contracts);
|
||||
const channelConfigs = normalizeChannelConfigs(raw.channelConfigs);
|
||||
|
||||
let uiHints: Record<string, PluginConfigUiHint> | undefined;
|
||||
if (isRecord(raw.uiHints)) {
|
||||
@@ -280,6 +320,7 @@ export function loadPluginManifest(
|
||||
version,
|
||||
uiHints,
|
||||
contracts,
|
||||
channelConfigs,
|
||||
},
|
||||
manifestPath,
|
||||
};
|
||||
|
||||
@@ -2,6 +2,29 @@ import { describe, expect, it } from "vitest";
|
||||
import { validateJsonSchemaValue } from "./schema-validator.js";
|
||||
|
||||
describe("schema validator", () => {
|
||||
it("can apply JSON Schema defaults while validating", () => {
|
||||
const res = validateJsonSchemaValue({
|
||||
cacheKey: "schema-validator.test.defaults",
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
mode: {
|
||||
type: "string",
|
||||
default: "auto",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
value: {},
|
||||
applyDefaults: true,
|
||||
});
|
||||
|
||||
expect(res.ok).toBe(true);
|
||||
if (res.ok) {
|
||||
expect(res.value).toEqual({ mode: "auto" });
|
||||
}
|
||||
});
|
||||
|
||||
it("includes allowed values in enum validation errors", () => {
|
||||
const res = validateJsonSchemaValue({
|
||||
cacheKey: "schema-validator.test.enum",
|
||||
|
||||
@@ -7,23 +7,26 @@ const require = createRequire(import.meta.url);
|
||||
type AjvLike = {
|
||||
compile: (schema: Record<string, unknown>) => ValidateFunction;
|
||||
};
|
||||
let ajvSingleton: AjvLike | null = null;
|
||||
const ajvSingletons = new Map<"default" | "defaults", AjvLike>();
|
||||
|
||||
function getAjv(): AjvLike {
|
||||
if (ajvSingleton) {
|
||||
return ajvSingleton;
|
||||
function getAjv(mode: "default" | "defaults"): AjvLike {
|
||||
const cached = ajvSingletons.get(mode);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const ajvModule = require("ajv") as { default?: new (opts?: object) => AjvLike };
|
||||
const AjvCtor =
|
||||
typeof ajvModule.default === "function"
|
||||
? ajvModule.default
|
||||
: (ajvModule as unknown as new (opts?: object) => AjvLike);
|
||||
ajvSingleton = new AjvCtor({
|
||||
const instance = new AjvCtor({
|
||||
allErrors: true,
|
||||
strict: false,
|
||||
removeAdditional: false,
|
||||
...(mode === "defaults" ? { useDefaults: true } : {}),
|
||||
});
|
||||
return ajvSingleton;
|
||||
ajvSingletons.set(mode, instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
type CachedValidator = {
|
||||
@@ -33,6 +36,13 @@ type CachedValidator = {
|
||||
|
||||
const schemaCache = new Map<string, CachedValidator>();
|
||||
|
||||
function cloneValidationValue<T>(value: T): T {
|
||||
if (value === undefined || value === null) {
|
||||
return value;
|
||||
}
|
||||
return structuredClone(value);
|
||||
}
|
||||
|
||||
export type JsonSchemaValidationError = {
|
||||
path: string;
|
||||
message: string;
|
||||
@@ -134,17 +144,20 @@ export function validateJsonSchemaValue(params: {
|
||||
schema: Record<string, unknown>;
|
||||
cacheKey: string;
|
||||
value: unknown;
|
||||
}): { ok: true } | { ok: false; errors: JsonSchemaValidationError[] } {
|
||||
let cached = schemaCache.get(params.cacheKey);
|
||||
applyDefaults?: boolean;
|
||||
}): { ok: true; value: unknown } | { ok: false; errors: JsonSchemaValidationError[] } {
|
||||
const cacheKey = params.applyDefaults ? `${params.cacheKey}::defaults` : params.cacheKey;
|
||||
let cached = schemaCache.get(cacheKey);
|
||||
if (!cached || cached.schema !== params.schema) {
|
||||
const validate = getAjv().compile(params.schema);
|
||||
const validate = getAjv(params.applyDefaults ? "defaults" : "default").compile(params.schema);
|
||||
cached = { validate, schema: params.schema };
|
||||
schemaCache.set(params.cacheKey, cached);
|
||||
schemaCache.set(cacheKey, cached);
|
||||
}
|
||||
|
||||
const ok = cached.validate(params.value);
|
||||
const value = params.applyDefaults ? cloneValidationValue(params.value) : params.value;
|
||||
const ok = cached.validate(value);
|
||||
if (ok) {
|
||||
return { ok: true };
|
||||
return { ok: true, value };
|
||||
}
|
||||
return { ok: false, errors: formatAjvErrors(cached.validate.errors) };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user