mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 04:10:00 +08:00
feat(channels): add Synology and Google Chat config
This commit is contained in:
@@ -2429,7 +2429,7 @@ function putWildcardAllowFromWhenOpen(entry, previousAllowFrom) {
|
||||
}
|
||||
|
||||
function platformSupportsTopLevelRequireMention(platform) {
|
||||
return ['feishu', 'slack', 'msteams', 'mattermost'].includes(platformStorageKey(platform))
|
||||
return ['feishu', 'slack', 'msteams', 'mattermost', 'googlechat'].includes(platformStorageKey(platform))
|
||||
}
|
||||
|
||||
export function normalizeMessagingPlatformForm(platform, form = {}) {
|
||||
@@ -2438,7 +2438,7 @@ export function normalizeMessagingPlatformForm(platform, form = {}) {
|
||||
if (!Object.hasOwn(normalized, 'allowFrom') && Object.hasOwn(normalized, 'allowedUsers')) {
|
||||
normalized.allowFrom = normalized.allowedUsers
|
||||
}
|
||||
const needsAccessDefaults = ['telegram', 'discord', 'feishu', 'slack', 'signal', 'msteams', 'whatsapp', 'zalo', 'zalouser', 'line', 'mattermost'].includes(storageKey)
|
||||
const needsAccessDefaults = ['telegram', 'discord', 'feishu', 'slack', 'signal', 'msteams', 'whatsapp', 'zalo', 'zalouser', 'line', 'mattermost', 'googlechat'].includes(storageKey)
|
||||
const hasDmField = Object.hasOwn(normalized, 'dmPolicy') || needsAccessDefaults
|
||||
const hasGroupField = Object.hasOwn(normalized, 'groupPolicy') || needsAccessDefaults
|
||||
|
||||
@@ -2468,7 +2468,11 @@ export function normalizeMessagingPlatformForm(platform, form = {}) {
|
||||
normalized.groupAllowFrom = csvToStringArray(normalized.groupAllowFrom)
|
||||
}
|
||||
|
||||
for (const key of ['mediaMaxMb', 'historyLimit']) {
|
||||
if (Object.hasOwn(normalized, 'allowedUserIds')) {
|
||||
normalized.allowedUserIds = csvToStringArray(normalized.allowedUserIds)
|
||||
}
|
||||
|
||||
for (const key of ['mediaMaxMb', 'historyLimit', 'dmHistoryLimit', 'textChunkLimit', 'rateLimitPerMinute']) {
|
||||
if (!Object.hasOwn(normalized, key)) continue
|
||||
const value = String(normalized[key] || '').trim()
|
||||
if (!value) {
|
||||
@@ -2481,7 +2485,7 @@ export function normalizeMessagingPlatformForm(platform, form = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of ['dangerouslyAllowNameMatching', 'dangerouslyAllowPrivateNetwork']) {
|
||||
for (const key of ['dangerouslyAllowNameMatching', 'dangerouslyAllowPrivateNetwork', 'dangerouslyAllowInheritedWebhookPath', 'allowInsecureSsl', 'allowBots', 'blockStreaming']) {
|
||||
if (Object.hasOwn(normalized, key)) {
|
||||
const value = String(normalized[key] || '').trim()
|
||||
if (!value) {
|
||||
@@ -2593,6 +2597,9 @@ const MESSAGING_CREDENTIAL_FIELDS = [
|
||||
'gatewayToken',
|
||||
'password',
|
||||
'secretFile',
|
||||
'serviceAccount',
|
||||
'serviceAccountFile',
|
||||
'serviceAccountRef',
|
||||
'signingSecret',
|
||||
'token',
|
||||
'tokenFile',
|
||||
@@ -2615,6 +2622,13 @@ function channelAnyCredentialFields(platform) {
|
||||
if (storageKey === 'zalo') {
|
||||
return [['botToken', 'Bot Token'], ['tokenFile', 'Token File']]
|
||||
}
|
||||
if (storageKey === 'googlechat') {
|
||||
return [
|
||||
['serviceAccountFile', 'Service Account File'],
|
||||
['serviceAccount', 'Service Account JSON'],
|
||||
['serviceAccountRef', 'Service Account SecretRef'],
|
||||
]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -2637,6 +2651,7 @@ const CHANNEL_DIAG_REQUIRED_FIELDS = {
|
||||
'dingtalk-connector': [['clientId', 'Client ID'], ['clientSecret', 'Client Secret']],
|
||||
msteams: [['appId', 'App ID'], ['appPassword', 'App Password']],
|
||||
mattermost: [['botToken', 'Bot Token'], ['baseUrl', 'Base URL']],
|
||||
'synology-chat': [['token', 'Token'], ['incomingUrl', 'Incoming URL']],
|
||||
signal: [['account', 'Signal 账号']],
|
||||
}
|
||||
|
||||
@@ -2918,6 +2933,42 @@ export function buildMessagingPlatformFormValues(platform, saved = {}, options =
|
||||
return form
|
||||
}
|
||||
|
||||
if (storageKey === 'synology-chat') {
|
||||
for (const key of ['token', 'incomingUrl', 'nasHost', 'webhookPath', 'botName']) {
|
||||
putSecretAwareFormValue(form, saved, key)
|
||||
}
|
||||
putStringFormValue(form, saved, 'dmPolicy')
|
||||
putCsvFormValue(form, saved, 'allowedUserIds')
|
||||
if (typeof saved.rateLimitPerMinute === 'number') form.rateLimitPerMinute = String(saved.rateLimitPerMinute)
|
||||
putBoolFormValue(form, saved, 'dangerouslyAllowNameMatching')
|
||||
putBoolFormValue(form, saved, 'dangerouslyAllowInheritedWebhookPath')
|
||||
putBoolFormValue(form, saved, 'allowInsecureSsl')
|
||||
return form
|
||||
}
|
||||
|
||||
if (storageKey === 'googlechat') {
|
||||
for (const key of ['serviceAccount', 'serviceAccountFile', 'serviceAccountRef', 'audienceType', 'audience', 'appPrincipal', 'webhookPath', 'webhookUrl', 'botUser', 'chunkMode', 'replyToMode', 'typingIndicator', 'responsePrefix']) {
|
||||
putSecretAwareFormValue(form, saved, key)
|
||||
}
|
||||
const dm = saved.dm && typeof saved.dm === 'object' ? saved.dm : {}
|
||||
putStringFormValue(form, dm, 'policy')
|
||||
if (form.policy && !form.dmPolicy) {
|
||||
form.dmPolicy = form.policy
|
||||
delete form.policy
|
||||
}
|
||||
putCsvFormValue(form, dm, 'allowFrom')
|
||||
putStringFormValue(form, saved, 'groupPolicy')
|
||||
putCsvFormValue(form, saved, 'groupAllowFrom')
|
||||
putBoolFormValue(form, saved, 'requireMention')
|
||||
putBoolFormValue(form, saved, 'dangerouslyAllowNameMatching')
|
||||
putBoolFormValue(form, saved, 'allowBots')
|
||||
putBoolFormValue(form, saved, 'blockStreaming')
|
||||
for (const key of ['historyLimit', 'dmHistoryLimit', 'textChunkLimit', 'mediaMaxMb']) {
|
||||
if (typeof saved[key] === 'number') form[key] = String(saved[key])
|
||||
}
|
||||
return form
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(saved)) {
|
||||
if (key === 'enabled' || key === 'accounts') continue
|
||||
if (typeof value === 'string') form[key] = value
|
||||
@@ -3479,6 +3530,32 @@ function buildOpenClawMessagingPlatformEntry(platform, form, currentSaved = {})
|
||||
if (form.callbackPath) commands.callbackPath = form.callbackPath
|
||||
if (form.callbackUrl) commands.callbackUrl = form.callbackUrl
|
||||
if (Object.keys(commands).length) entry.commands = { ...(currentSaved?.commands || {}), ...commands }
|
||||
} else if (storageKey === 'synology-chat') {
|
||||
for (const key of ['token', 'incomingUrl', 'nasHost', 'webhookPath', 'botName']) {
|
||||
if (form[key]) entry[key] = form[key]
|
||||
}
|
||||
entry.dmPolicy = form.dmPolicy || 'allowlist'
|
||||
if (Array.isArray(form.allowedUserIds) && form.allowedUserIds.length) entry.allowedUserIds = form.allowedUserIds
|
||||
if (typeof form.rateLimitPerMinute === 'number') entry.rateLimitPerMinute = form.rateLimitPerMinute
|
||||
for (const key of ['dangerouslyAllowNameMatching', 'dangerouslyAllowInheritedWebhookPath', 'allowInsecureSsl']) {
|
||||
if (typeof form[key] === 'boolean') entry[key] = form[key]
|
||||
}
|
||||
} else if (storageKey === 'googlechat') {
|
||||
for (const key of ['serviceAccount', 'serviceAccountFile', 'serviceAccountRef', 'audienceType', 'audience', 'appPrincipal', 'webhookPath', 'webhookUrl', 'botUser', 'chunkMode', 'replyToMode', 'typingIndicator', 'responsePrefix']) {
|
||||
if (form[key]) entry[key] = form[key]
|
||||
}
|
||||
const dm = { ...(currentSaved?.dm && typeof currentSaved.dm === 'object' ? currentSaved.dm : {}) }
|
||||
if (form.dmPolicy) dm.policy = form.dmPolicy
|
||||
if (Array.isArray(form.allowFrom)) dm.allowFrom = form.allowFrom
|
||||
if (Object.keys(dm).length) entry.dm = dm
|
||||
entry.groupPolicy = form.groupPolicy
|
||||
if (Array.isArray(form.groupAllowFrom) && form.groupAllowFrom.length) entry.groupAllowFrom = form.groupAllowFrom
|
||||
for (const key of ['dangerouslyAllowNameMatching', 'requireMention', 'allowBots', 'blockStreaming']) {
|
||||
if (typeof form[key] === 'boolean') entry[key] = form[key]
|
||||
}
|
||||
for (const key of ['historyLimit', 'dmHistoryLimit', 'textChunkLimit', 'mediaMaxMb']) {
|
||||
if (typeof form[key] === 'number') entry[key] = form[key]
|
||||
}
|
||||
} else {
|
||||
Object.assign(entry, form)
|
||||
}
|
||||
@@ -3494,6 +3571,9 @@ export function mergeOpenClawMessagingPlatformConfig(cfg, { platform, form, acco
|
||||
const currentSaved = resolvePlatformConfigEntry(cfg.channels?.[storageKey], platform, normalizedAccountId) || {}
|
||||
const entry = buildOpenClawMessagingPlatformEntry(platform, normalizedForm, currentSaved)
|
||||
applyMessagingPlatformEntry(cfg, storageKey, normalizedAccountId, entry)
|
||||
if (['zalo', 'zalouser', 'line', 'mattermost', 'synology-chat', 'googlechat'].includes(storageKey)) {
|
||||
ensureMessagingPluginAllowed(cfg, storageKey)
|
||||
}
|
||||
return { entry, accountId: normalizedAccountId, storageKey }
|
||||
}
|
||||
|
||||
@@ -4961,16 +5041,16 @@ const handlers = {
|
||||
} else {
|
||||
setRootChannelEntry(entry)
|
||||
}
|
||||
} else if (platform === 'line' || platform === 'mattermost') {
|
||||
} else if (['line', 'mattermost', 'synology-chat', 'googlechat'].includes(storageKey)) {
|
||||
const built = buildOpenClawMessagingPlatformEntry(platform, form, currentSaved)
|
||||
applyMessagingPlatformEntry(cfg, storageKey, normalizedAccountId, built)
|
||||
ensureMessagingPluginAllowed(cfg, platform)
|
||||
ensureMessagingPluginAllowed(cfg, storageKey)
|
||||
} else {
|
||||
Object.assign(entry, form)
|
||||
preserveMessagingCredentialRefs(entry, form, currentSaved)
|
||||
}
|
||||
|
||||
if (platform !== 'qqbot' && platform !== 'feishu' && platform !== 'dingtalk' && platform !== 'dingtalk-connector' && platform !== 'line' && platform !== 'mattermost') {
|
||||
if (platform !== 'qqbot' && platform !== 'feishu' && platform !== 'dingtalk' && platform !== 'dingtalk-connector' && !['line', 'mattermost', 'synology-chat', 'googlechat'].includes(storageKey)) {
|
||||
preserveMessagingCredentialRefs(entry, form, currentSaved)
|
||||
// 合并模式:保留用户通过 CLI 或手动编辑的自定义字段
|
||||
applyMessagingPlatformEntry(cfg, storageKey, normalizedAccountId, entry)
|
||||
|
||||
@@ -148,6 +148,9 @@ fn preserve_messaging_credential_refs(
|
||||
"gatewayToken",
|
||||
"password",
|
||||
"secretFile",
|
||||
"serviceAccount",
|
||||
"serviceAccountFile",
|
||||
"serviceAccountRef",
|
||||
"signingSecret",
|
||||
"token",
|
||||
"tokenFile",
|
||||
@@ -192,6 +195,9 @@ fn channel_root_has_messaging_credential(root: &Map<String, Value>) -> bool {
|
||||
"gatewayToken",
|
||||
"password",
|
||||
"secretFile",
|
||||
"serviceAccount",
|
||||
"serviceAccountFile",
|
||||
"serviceAccountRef",
|
||||
"signingSecret",
|
||||
"token",
|
||||
"tokenFile",
|
||||
@@ -219,6 +225,7 @@ fn required_channel_credential_fields(
|
||||
"dingtalk-connector" => vec![("clientId", "Client ID"), ("clientSecret", "Client Secret")],
|
||||
"msteams" => vec![("appId", "App ID"), ("appPassword", "App Password")],
|
||||
"mattermost" => vec![("botToken", "Bot Token"), ("baseUrl", "Base URL")],
|
||||
"synology-chat" => vec![("token", "Token"), ("incomingUrl", "Incoming URL")],
|
||||
"signal" => vec![("account", "Signal 账号")],
|
||||
"slack" => {
|
||||
let mode = form_string(form, "mode");
|
||||
@@ -249,6 +256,11 @@ fn required_channel_credential_fields(
|
||||
fn channel_any_credential_fields(platform: &str) -> Vec<(&'static str, &'static str)> {
|
||||
match platform_storage_key(platform) {
|
||||
"zalo" => vec![("botToken", "Bot Token"), ("tokenFile", "Token File")],
|
||||
"googlechat" => vec![
|
||||
("serviceAccountFile", "Service Account File"),
|
||||
("serviceAccount", "Service Account JSON"),
|
||||
("serviceAccountRef", "Service Account SecretRef"),
|
||||
],
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
@@ -701,7 +713,7 @@ fn normalize_group_policy_value(raw: Option<&Value>, fallback: &str) -> String {
|
||||
fn platform_supports_top_level_require_mention(platform: &str) -> bool {
|
||||
matches!(
|
||||
platform_storage_key(platform),
|
||||
"feishu" | "slack" | "msteams" | "mattermost"
|
||||
"feishu" | "slack" | "msteams" | "mattermost" | "googlechat"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -731,6 +743,7 @@ fn normalize_messaging_platform_form(
|
||||
| "zalouser"
|
||||
| "line"
|
||||
| "mattermost"
|
||||
| "googlechat"
|
||||
);
|
||||
let has_dm_field = normalized.contains_key("dmPolicy") || needs_access_defaults;
|
||||
let has_group_field = normalized.contains_key("groupPolicy") || needs_access_defaults;
|
||||
@@ -786,12 +799,24 @@ fn normalize_messaging_platform_form(
|
||||
normalized.insert("groupAllowFrom".into(), Value::Array(items));
|
||||
}
|
||||
|
||||
if normalized.contains_key("allowedUserIds") {
|
||||
let items = json_array_from_csv_value(normalized.get("allowedUserIds"));
|
||||
normalized.insert("allowedUserIds".into(), Value::Array(items));
|
||||
}
|
||||
|
||||
normalize_numeric_form_value(&mut normalized, "mediaMaxMb");
|
||||
normalize_numeric_form_value(&mut normalized, "historyLimit");
|
||||
normalize_numeric_form_value(&mut normalized, "dmHistoryLimit");
|
||||
normalize_numeric_form_value(&mut normalized, "textChunkLimit");
|
||||
normalize_numeric_form_value(&mut normalized, "rateLimitPerMinute");
|
||||
|
||||
for key in [
|
||||
"dangerouslyAllowNameMatching",
|
||||
"dangerouslyAllowPrivateNetwork",
|
||||
"dangerouslyAllowInheritedWebhookPath",
|
||||
"allowInsecureSsl",
|
||||
"allowBots",
|
||||
"blockStreaming",
|
||||
] {
|
||||
if normalized.contains_key(key) {
|
||||
let value = match normalized.get(key) {
|
||||
@@ -1452,6 +1477,60 @@ pub async fn read_platform_config(
|
||||
insert_string_if_present(&mut form, commands, "callbackUrl");
|
||||
}
|
||||
}
|
||||
"synology-chat" => {
|
||||
for key in ["token", "incomingUrl", "nasHost", "webhookPath", "botName"] {
|
||||
insert_secret_aware_form_value(&mut form, &saved, key);
|
||||
}
|
||||
insert_string_if_present(&mut form, &saved, "dmPolicy");
|
||||
insert_array_as_csv(&mut form, &saved, "allowedUserIds");
|
||||
if let Some(v) = saved.get("rateLimitPerMinute").and_then(|v| v.as_i64()) {
|
||||
form.insert("rateLimitPerMinute".into(), Value::String(v.to_string()));
|
||||
}
|
||||
insert_bool_as_string(&mut form, &saved, "dangerouslyAllowNameMatching");
|
||||
insert_bool_as_string(&mut form, &saved, "dangerouslyAllowInheritedWebhookPath");
|
||||
insert_bool_as_string(&mut form, &saved, "allowInsecureSsl");
|
||||
}
|
||||
"googlechat" => {
|
||||
for key in [
|
||||
"serviceAccount",
|
||||
"serviceAccountFile",
|
||||
"serviceAccountRef",
|
||||
"audienceType",
|
||||
"audience",
|
||||
"appPrincipal",
|
||||
"webhookPath",
|
||||
"webhookUrl",
|
||||
"botUser",
|
||||
"chunkMode",
|
||||
"replyToMode",
|
||||
"typingIndicator",
|
||||
"responsePrefix",
|
||||
] {
|
||||
insert_secret_aware_form_value(&mut form, &saved, key);
|
||||
}
|
||||
if let Some(dm) = saved.get("dm") {
|
||||
if let Some(policy) = dm.get("policy").and_then(|v| v.as_str()) {
|
||||
form.insert("dmPolicy".into(), Value::String(policy.into()));
|
||||
}
|
||||
insert_array_as_csv(&mut form, dm, "allowFrom");
|
||||
}
|
||||
insert_string_if_present(&mut form, &saved, "groupPolicy");
|
||||
insert_array_as_csv(&mut form, &saved, "groupAllowFrom");
|
||||
insert_bool_as_string(&mut form, &saved, "requireMention");
|
||||
insert_bool_as_string(&mut form, &saved, "dangerouslyAllowNameMatching");
|
||||
insert_bool_as_string(&mut form, &saved, "allowBots");
|
||||
insert_bool_as_string(&mut form, &saved, "blockStreaming");
|
||||
for key in [
|
||||
"historyLimit",
|
||||
"dmHistoryLimit",
|
||||
"textChunkLimit",
|
||||
"mediaMaxMb",
|
||||
] {
|
||||
if let Some(v) = saved.get(key).and_then(|v| v.as_f64()) {
|
||||
form.insert(key.into(), Value::String(v.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if saved.is_null() {
|
||||
return Ok(json!({ "exists": false }));
|
||||
@@ -2226,6 +2305,148 @@ pub async fn save_messaging_platform(
|
||||
)?;
|
||||
ensure_plugin_allowed(&mut cfg, "mattermost")?;
|
||||
}
|
||||
"synology-chat" => {
|
||||
let token = form_string(form_obj, "token");
|
||||
let incoming_url = form_string(form_obj, "incomingUrl");
|
||||
if token.is_empty() {
|
||||
return Err("Synology Chat Token 不能为空".into());
|
||||
}
|
||||
if incoming_url.is_empty() {
|
||||
return Err("Synology Chat Incoming URL 不能为空".into());
|
||||
}
|
||||
|
||||
let mut entry = Map::new();
|
||||
entry.insert("enabled".into(), Value::Bool(true));
|
||||
put_string(&mut entry, "token", token);
|
||||
put_string(&mut entry, "incomingUrl", incoming_url);
|
||||
put_string(&mut entry, "nasHost", form_string(form_obj, "nasHost"));
|
||||
put_string(
|
||||
&mut entry,
|
||||
"webhookPath",
|
||||
form_string(form_obj, "webhookPath"),
|
||||
);
|
||||
put_string(&mut entry, "botName", form_string(form_obj, "botName"));
|
||||
put_string(&mut entry, "dmPolicy", form_string(form_obj, "dmPolicy"));
|
||||
put_array_from_form_value(&mut entry, "allowedUserIds", form_obj.get("allowedUserIds"));
|
||||
if let Some(value) = form_obj.get("rateLimitPerMinute").and_then(|v| v.as_f64()) {
|
||||
if let Some(number) = serde_json::Number::from_f64(value) {
|
||||
entry.insert("rateLimitPerMinute".into(), Value::Number(number));
|
||||
}
|
||||
} else {
|
||||
put_number_from_form(
|
||||
&mut entry,
|
||||
"rateLimitPerMinute",
|
||||
&form_string(form_obj, "rateLimitPerMinute"),
|
||||
);
|
||||
}
|
||||
put_bool_value_if_present(
|
||||
&mut entry,
|
||||
"dangerouslyAllowNameMatching",
|
||||
form_obj.get("dangerouslyAllowNameMatching"),
|
||||
);
|
||||
put_bool_value_if_present(
|
||||
&mut entry,
|
||||
"dangerouslyAllowInheritedWebhookPath",
|
||||
form_obj.get("dangerouslyAllowInheritedWebhookPath"),
|
||||
);
|
||||
put_bool_value_if_present(
|
||||
&mut entry,
|
||||
"allowInsecureSsl",
|
||||
form_obj.get("allowInsecureSsl"),
|
||||
);
|
||||
preserve_messaging_credential_refs(&mut entry, form_obj, ¤t_saved);
|
||||
merge_channel_entry_for_account(
|
||||
channels_map,
|
||||
&storage_key,
|
||||
account_id.as_deref(),
|
||||
entry,
|
||||
)?;
|
||||
ensure_plugin_allowed(&mut cfg, "synology-chat")?;
|
||||
}
|
||||
"googlechat" => {
|
||||
let has_service_account =
|
||||
has_configured_messaging_value(form_obj.get("serviceAccount"))
|
||||
|| has_configured_messaging_value(form_obj.get("serviceAccountFile"))
|
||||
|| has_configured_messaging_value(form_obj.get("serviceAccountRef"));
|
||||
if !has_service_account {
|
||||
return Err(
|
||||
"Google Chat 需要填写 Service Account JSON、Service Account File 或 SecretRef"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut entry = Map::new();
|
||||
entry.insert("enabled".into(), Value::Bool(true));
|
||||
for key in [
|
||||
"serviceAccount",
|
||||
"serviceAccountFile",
|
||||
"serviceAccountRef",
|
||||
"audienceType",
|
||||
"audience",
|
||||
"appPrincipal",
|
||||
"webhookPath",
|
||||
"webhookUrl",
|
||||
"botUser",
|
||||
"chunkMode",
|
||||
"replyToMode",
|
||||
"typingIndicator",
|
||||
"responsePrefix",
|
||||
] {
|
||||
put_string(&mut entry, key, form_string(form_obj, key));
|
||||
}
|
||||
|
||||
let mut dm = current_saved
|
||||
.get("dm")
|
||||
.and_then(|v| v.as_object())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
put_string(&mut dm, "policy", form_string(form_obj, "dmPolicy"));
|
||||
let allow_from = json_array_from_csv_value(form_obj.get("allowFrom"));
|
||||
if !allow_from.is_empty() {
|
||||
dm.insert("allowFrom".into(), Value::Array(allow_from));
|
||||
}
|
||||
if !dm.is_empty() {
|
||||
entry.insert("dm".into(), Value::Object(dm));
|
||||
}
|
||||
|
||||
put_string(
|
||||
&mut entry,
|
||||
"groupPolicy",
|
||||
form_string(form_obj, "groupPolicy"),
|
||||
);
|
||||
put_array_from_form_value(&mut entry, "groupAllowFrom", form_obj.get("groupAllowFrom"));
|
||||
for key in [
|
||||
"dangerouslyAllowNameMatching",
|
||||
"requireMention",
|
||||
"allowBots",
|
||||
"blockStreaming",
|
||||
] {
|
||||
put_bool_value_if_present(&mut entry, key, form_obj.get(key));
|
||||
}
|
||||
for key in [
|
||||
"historyLimit",
|
||||
"dmHistoryLimit",
|
||||
"textChunkLimit",
|
||||
"mediaMaxMb",
|
||||
] {
|
||||
if let Some(value) = form_obj.get(key).and_then(|v| v.as_f64()) {
|
||||
if let Some(number) = serde_json::Number::from_f64(value) {
|
||||
entry.insert(key.into(), Value::Number(number));
|
||||
}
|
||||
} else {
|
||||
put_number_from_form(&mut entry, key, &form_string(form_obj, key));
|
||||
}
|
||||
}
|
||||
|
||||
preserve_messaging_credential_refs(&mut entry, form_obj, ¤t_saved);
|
||||
merge_channel_entry_for_account(
|
||||
channels_map,
|
||||
&storage_key,
|
||||
account_id.as_deref(),
|
||||
entry,
|
||||
)?;
|
||||
ensure_plugin_allowed(&mut cfg, "googlechat")?;
|
||||
}
|
||||
_ => {
|
||||
// 通用平台:直接保存表单字段
|
||||
let mut entry = Map::new();
|
||||
|
||||
@@ -935,11 +935,48 @@
|
||||
"matrixPasswordPh": "Leave empty when using Access Token",
|
||||
"matrixAllowFromPh": "Optional, comma-separated user IDs",
|
||||
"matrixAuthRequired": "Matrix requires an Access Token, or User ID + Password",
|
||||
"synologyChatDesc": "Connect Synology Chat for NAS-hosted team messaging",
|
||||
"synologyChatGuide1": "Create a bot in Synology Chat administration and copy its Token",
|
||||
"synologyChatGuide2": "Configure an Incoming Webhook or bot post URL, then paste it as Incoming URL",
|
||||
"synologyChatGuide3": "Configure the Outgoing Webhook path in Synology Chat; /webhook/synology is recommended",
|
||||
"synologyChatGuide4": "Save, reload Gateway, then test from the target channel",
|
||||
"synologyChatGuideFooter": "<div style=\"margin-top:8px;font-size:var(--font-size-xs);color:var(--text-tertiary)\">For private NAS networks, make sure Gateway can reach the NAS host.</div>",
|
||||
"synologyChatTokenPh": "Synology Chat Bot Token",
|
||||
"synologyChatIncomingUrlHint": "Synology Chat Incoming Webhook URL used to send bot replies",
|
||||
"synologyChatAllowedUserIdsHint": "Comma-separated DM allowlist user IDs; used when dmPolicy=allowlist",
|
||||
"synologyChatNameMatching": "Allow name matching",
|
||||
"synologyChatNameMatchingHint": "Use only when stable user IDs are unavailable; IDs are safer",
|
||||
"synologyChatInheritedWebhookPath": "Allow inherited Webhook Path",
|
||||
"synologyChatInheritedWebhookPathHint": "Enable when multiple accounts share the root webhookPath",
|
||||
"synologyChatAllowInsecureSsl": "Allow insecure SSL",
|
||||
"synologyChatAllowInsecureSslHint": "Use only for self-signed certificates or trusted internal testing",
|
||||
"googleChatDesc": "Connect a Google Chat App for spaces and direct messages",
|
||||
"googleChatGuide1": "Create a Chat App in Google Cloud and enable the Google Chat API",
|
||||
"googleChatGuide2": "Create a Service Account, then download its JSON file or prepare inline JSON",
|
||||
"googleChatGuide3": "Configure the Google Chat callback URL and set audienceType plus audience",
|
||||
"googleChatGuide4": "Save, reload Gateway, then invite the app to a target Space for testing",
|
||||
"googleChatGuideFooter": "<div style=\"margin-top:8px;font-size:var(--font-size-xs);color:var(--text-tertiary)\">Prefer Service Account File in production to avoid storing long JSON inline.</div>",
|
||||
"googleChatServiceAccountFileHint": "Path to the Service Account JSON file; recommended for production",
|
||||
"googleChatServiceAccountHint": "Paste Service Account JSON inline; it will be written to openclaw.json",
|
||||
"googleChatServiceAccountRefHint": "Preserve or enter an existing SecretRef, e.g. SecretRef(env:default:GOOGLE_CHAT_SERVICE_ACCOUNT)",
|
||||
"googleChatAudienceHint": "Use callback URL for app-url mode; use Google Cloud project number for project-number mode",
|
||||
"googleChatAllowFromHint": "DM allowlist, using users/<id> or email entries separated by commas",
|
||||
"googleChatGroupAllowFromHint": "Space allowlist, using spaces/<id> entries separated by commas",
|
||||
"googleChatNameMatching": "Allow name matching",
|
||||
"googleChatNameMatchingHint": "Stable IDs are preferred; enable only when email/name matching is required",
|
||||
"googleChatRequireMention": "Require mention in groups",
|
||||
"googleChatServiceAccountRequired": "Service Account File / JSON / SecretRef",
|
||||
"groupAllGroups": "All groups",
|
||||
"groupAllRooms": "All rooms",
|
||||
"groupAllTeams": "All teams",
|
||||
"groupAllSpaces": "All Spaces",
|
||||
"groupMentionBot": "Only when @bot",
|
||||
"optionalEg": "Optional, e.g.",
|
||||
"groupDisabled": "Disable groups",
|
||||
"dmPairing": "Pairing approval",
|
||||
"dmOpen": "Allow all DMs",
|
||||
"dmAllowlist": "Allowlist",
|
||||
"dmDisabled": "Disable DMs",
|
||||
"optionalEg": "Optional, e.g. {example}",
|
||||
"editAccountLabel": "Edit {id}",
|
||||
"bound": "Bound",
|
||||
"notBoundAgent": "No Agent bound",
|
||||
|
||||
@@ -128,6 +128,37 @@ export default {
|
||||
mattermostNameMatchingHint: _('关闭时优先使用稳定 ID,避免同名用户或频道误匹配。', 'When disabled, prefer stable IDs to avoid wrong matches with duplicate users or channels.'),
|
||||
mattermostPrivateNetwork: _('允许内网地址', 'Allow Private Network'),
|
||||
mattermostPrivateNetworkHint: _('仅在 Mattermost 部署于可信内网时开启。', 'Enable only when Mattermost is deployed on a trusted private network.'),
|
||||
synologyChatDesc: _('接入群晖 Synology Chat,适合 NAS 内网团队协作', 'Connect Synology Chat for NAS-hosted team messaging'),
|
||||
synologyChatGuide1: _('在 Synology Chat 管理后台创建 Bot,并复制 Token', 'Create a bot in Synology Chat administration and copy its Token'),
|
||||
synologyChatGuide2: _('配置 Incoming Webhook 或机器人发消息 URL,填入 Incoming URL', 'Configure an Incoming Webhook or bot post URL, then paste it as Incoming URL'),
|
||||
synologyChatGuide3: _('在 Synology Chat 中配置 Outgoing Webhook,路径建议使用 /webhook/synology', 'Configure the Outgoing Webhook path in Synology Chat; /webhook/synology is recommended'),
|
||||
synologyChatGuide4: _('保存后重启或重载 Gateway,再在目标频道测试消息', 'Save, reload Gateway, then test from the target channel'),
|
||||
synologyChatGuideFooter: _('<div style="margin-top:8px;font-size:var(--font-size-xs);color:var(--text-tertiary)">内网 NAS 场景如需访问私有地址,请确认 Gateway 网络能访问 NAS。</div>', '<div style="margin-top:8px;font-size:var(--font-size-xs);color:var(--text-tertiary)">For private NAS networks, make sure Gateway can reach the NAS host.</div>'),
|
||||
synologyChatTokenPh: _('Synology Chat Bot Token', 'Synology Chat Bot Token'),
|
||||
synologyChatIncomingUrlHint: _('Synology Chat Incoming Webhook URL,用于机器人发送回复', 'Synology Chat Incoming Webhook URL used to send bot replies'),
|
||||
synologyChatAllowedUserIdsHint: _('DM 白名单用户 ID,逗号分隔;dmPolicy=allowlist 时生效', 'Comma-separated DM allowlist user IDs; used when dmPolicy=allowlist'),
|
||||
synologyChatNameMatching: _('允许名称匹配', 'Allow name matching'),
|
||||
synologyChatNameMatchingHint: _('仅在无法稳定获取用户 ID 时使用;更推荐填写用户 ID', 'Use only when stable user IDs are unavailable; IDs are safer'),
|
||||
synologyChatInheritedWebhookPath: _('允许继承 Webhook Path', 'Allow inherited Webhook Path'),
|
||||
synologyChatInheritedWebhookPathHint: _('多账号共享根配置 webhookPath 时开启', 'Enable when multiple accounts share the root webhookPath'),
|
||||
synologyChatAllowInsecureSsl: _('允许不安全 SSL', 'Allow insecure SSL'),
|
||||
synologyChatAllowInsecureSslHint: _('仅用于自签名证书或内网测试环境', 'Use only for self-signed certificates or trusted internal testing'),
|
||||
googleChatDesc: _('接入 Google Chat App,支持空间消息和私聊', 'Connect a Google Chat App for spaces and direct messages'),
|
||||
googleChatGuide1: _('在 Google Cloud 中创建 Chat App,并启用 Google Chat API', 'Create a Chat App in Google Cloud and enable the Google Chat API'),
|
||||
googleChatGuide2: _('创建 Service Account,下载 JSON 文件或准备内联 JSON', 'Create a Service Account, then download its JSON file or prepare inline JSON'),
|
||||
googleChatGuide3: _('配置 Google Chat 回调 URL,并设置 audienceType 与 audience', 'Configure the Google Chat callback URL and set audienceType plus audience'),
|
||||
googleChatGuide4: _('保存后重启或重载 Gateway,再邀请应用到目标 Space 测试', 'Save, reload Gateway, then invite the app to a target Space for testing'),
|
||||
googleChatGuideFooter: _('<div style="margin-top:8px;font-size:var(--font-size-xs);color:var(--text-tertiary)">建议优先使用 Service Account File,避免在配置文件中直接粘贴长 JSON。</div>', '<div style="margin-top:8px;font-size:var(--font-size-xs);color:var(--text-tertiary)">Prefer Service Account File in production to avoid storing long JSON inline.</div>'),
|
||||
googleChatServiceAccountFileHint: _('Service Account JSON 文件路径,推荐用于生产环境', 'Path to the Service Account JSON file; recommended for production'),
|
||||
googleChatServiceAccountHint: _('可直接粘贴 Service Account JSON;保存后会写入 openclaw.json', 'Paste Service Account JSON inline; it will be written to openclaw.json'),
|
||||
googleChatServiceAccountRefHint: _('已有 SecretRef 时可保留或手动填写,如 SecretRef(env:default:GOOGLE_CHAT_SERVICE_ACCOUNT)', 'Preserve or enter an existing SecretRef, e.g. SecretRef(env:default:GOOGLE_CHAT_SERVICE_ACCOUNT)'),
|
||||
googleChatAudienceHint: _('app-url 模式填回调 URL;project-number 模式填 Google Cloud 项目编号', 'Use callback URL for app-url mode; use Google Cloud project number for project-number mode'),
|
||||
googleChatAllowFromHint: _('DM 白名单,使用 users/<id> 或邮箱,逗号分隔', 'DM allowlist, using users/<id> or email entries separated by commas'),
|
||||
googleChatGroupAllowFromHint: _('Space 白名单,使用 spaces/<id>,逗号分隔', 'Space allowlist, using spaces/<id> entries separated by commas'),
|
||||
googleChatNameMatching: _('允许名称匹配', 'Allow name matching'),
|
||||
googleChatNameMatchingHint: _('默认建议使用稳定 ID;仅在邮箱/名称匹配必需时开启', 'Stable IDs are preferred; enable only when email/name matching is required'),
|
||||
googleChatRequireMention: _('群组要求提及', 'Require mention in groups'),
|
||||
googleChatServiceAccountRequired: _('Service Account File / JSON / SecretRef', 'Service Account File / JSON / SecretRef'),
|
||||
discordDesc: _('接入 Discord Bot,支持服务器频道和私信', 'Connect a Discord Bot, supports server channels and DMs', '接入 Discord Bot,支援伺服器頻道和私信', 'Discord Bot に接続'),
|
||||
discordGuide1: _('前往 <a href="https://discord.com/developers/applications" target="_blank" rel="noopener">Discord Developer Portal</a> 创建 Application', '前往 <a href="https://discord.com/developers/applications" target="_blank" rel="noopener">Discord Developer Portal</a> 创建 Application', '前往 <a href="https://discord.com/developers/applications" target="_blank" rel="noopener">Discord Developer Portal</a> 建立 Application'),
|
||||
discordGuide2: _('在 Bot 页面点击「Reset Token」获取 <strong>Bot Token</strong>', 'Click "Reset Token" on the Bot page to get the <strong>Bot Token</strong>', '在 Bot 頁面点擊「Reset Token」取得 <strong>Bot Token</strong>'),
|
||||
@@ -159,6 +190,7 @@ export default {
|
||||
dmDisabled: _('禁用私信', 'Disable DMs', '停用私信'),
|
||||
groupPolicy: _('群组策略', 'Group Policy', '群組策略'),
|
||||
groupAllChannels: _('所有频道', 'All channels', '所有頻道'),
|
||||
groupAllSpaces: _('所有 Space', 'All Spaces'),
|
||||
groupMentionOnly: _('仅 @提及时', 'Only when @mentioned', '僅 @提及時'),
|
||||
groupAllowlist: _('白名单', 'Allowlist', '白名單'),
|
||||
groupDisabled: _('禁用群组', 'Disable groups', '停用群組'),
|
||||
|
||||
@@ -1018,11 +1018,48 @@
|
||||
"matrixPasswordPh": "使用 Access Token 时可留空",
|
||||
"matrixAllowFromPh": "可选,逗号分隔用户 ID",
|
||||
"matrixAuthRequired": "Matrix 需要填写 Access Token,或填写 User ID + Password",
|
||||
"synologyChatDesc": "接入群晖 Synology Chat,适合 NAS 内网团队协作",
|
||||
"synologyChatGuide1": "在 Synology Chat 管理后台创建 Bot,并复制 Token",
|
||||
"synologyChatGuide2": "配置 Incoming Webhook 或机器人发消息 URL,填入 Incoming URL",
|
||||
"synologyChatGuide3": "在 Synology Chat 中配置 Outgoing Webhook,路径建议使用 /webhook/synology",
|
||||
"synologyChatGuide4": "保存后重启或重载 Gateway,再在目标频道测试消息",
|
||||
"synologyChatGuideFooter": "<div style=\"margin-top:8px;font-size:var(--font-size-xs);color:var(--text-tertiary)\">内网 NAS 场景如需访问私有地址,请确认 Gateway 网络能访问 NAS。</div>",
|
||||
"synologyChatTokenPh": "Synology Chat Bot Token",
|
||||
"synologyChatIncomingUrlHint": "Synology Chat Incoming Webhook URL,用于机器人发送回复",
|
||||
"synologyChatAllowedUserIdsHint": "DM 白名单用户 ID,逗号分隔;dmPolicy=allowlist 时生效",
|
||||
"synologyChatNameMatching": "允许名称匹配",
|
||||
"synologyChatNameMatchingHint": "仅在无法稳定获取用户 ID 时使用;更推荐填写用户 ID",
|
||||
"synologyChatInheritedWebhookPath": "允许继承 Webhook Path",
|
||||
"synologyChatInheritedWebhookPathHint": "多账号共享根配置 webhookPath 时开启",
|
||||
"synologyChatAllowInsecureSsl": "允许不安全 SSL",
|
||||
"synologyChatAllowInsecureSslHint": "仅用于自签名证书或内网测试环境",
|
||||
"googleChatDesc": "接入 Google Chat App,支持空间消息和私聊",
|
||||
"googleChatGuide1": "在 Google Cloud 中创建 Chat App,并启用 Google Chat API",
|
||||
"googleChatGuide2": "创建 Service Account,下载 JSON 文件或准备内联 JSON",
|
||||
"googleChatGuide3": "配置 Google Chat 回调 URL,并设置 audienceType 与 audience",
|
||||
"googleChatGuide4": "保存后重启或重载 Gateway,再邀请应用到目标 Space 测试",
|
||||
"googleChatGuideFooter": "<div style=\"margin-top:8px;font-size:var(--font-size-xs);color:var(--text-tertiary)\">建议优先使用 Service Account File,避免在配置文件中直接粘贴长 JSON。</div>",
|
||||
"googleChatServiceAccountFileHint": "Service Account JSON 文件路径,推荐用于生产环境",
|
||||
"googleChatServiceAccountHint": "可直接粘贴 Service Account JSON;保存后会写入 openclaw.json",
|
||||
"googleChatServiceAccountRefHint": "已有 SecretRef 时可保留或手动填写,如 SecretRef(env:default:GOOGLE_CHAT_SERVICE_ACCOUNT)",
|
||||
"googleChatAudienceHint": "app-url 模式填回调 URL;project-number 模式填 Google Cloud 项目编号",
|
||||
"googleChatAllowFromHint": "DM 白名单,使用 users/<id> 或邮箱,逗号分隔",
|
||||
"googleChatGroupAllowFromHint": "Space 白名单,使用 spaces/<id>,逗号分隔",
|
||||
"googleChatNameMatching": "允许名称匹配",
|
||||
"googleChatNameMatchingHint": "默认建议使用稳定 ID;仅在邮箱/名称匹配必需时开启",
|
||||
"googleChatRequireMention": "群组要求提及",
|
||||
"googleChatServiceAccountRequired": "Service Account File / JSON / SecretRef",
|
||||
"groupAllGroups": "所有群组",
|
||||
"groupAllRooms": "所有房间",
|
||||
"groupAllTeams": "所有团队",
|
||||
"groupAllSpaces": "所有 Space",
|
||||
"groupMentionBot": "仅 @机器人时",
|
||||
"optionalEg": "可选,如",
|
||||
"groupDisabled": "禁用群组",
|
||||
"dmPairing": "配对审批",
|
||||
"dmOpen": "允许所有私信",
|
||||
"dmAllowlist": "白名单",
|
||||
"dmDisabled": "禁用私信",
|
||||
"optionalEg": "可选,如 {example}",
|
||||
"editAccountLabel": "编辑 {id}",
|
||||
"bound": "已绑定",
|
||||
"notBoundAgent": "未绑定 Agent",
|
||||
|
||||
@@ -28,6 +28,13 @@ const DM_POLICY_OPTIONS = [
|
||||
{ value: 'disabled', label: t('channels.dmDisabled') },
|
||||
]
|
||||
|
||||
const SYNOLOGY_DM_POLICY_OPTIONS = [
|
||||
{ value: '', label: t('channels.policyDefault') },
|
||||
{ value: 'open', label: t('channels.dmOpen') },
|
||||
{ value: 'allowlist', label: t('channels.dmAllowlist') },
|
||||
{ value: 'disabled', label: t('channels.dmDisabled') },
|
||||
]
|
||||
|
||||
const GROUP_POLICY_OPTIONS = (allLabel, { mention = false } = {}) => [
|
||||
{ value: '', label: t('channels.policyDefault') },
|
||||
{ value: 'open', label: allLabel },
|
||||
@@ -267,6 +274,72 @@ const PLATFORM_REGISTRY = {
|
||||
pluginRequired: '@openclaw/mattermost@latest',
|
||||
pluginId: 'mattermost',
|
||||
},
|
||||
'synology-chat': {
|
||||
label: 'Synology Chat',
|
||||
iconName: 'message-square',
|
||||
desc: t('channels.synologyChatDesc'),
|
||||
guide: [
|
||||
t('channels.synologyChatGuide1'),
|
||||
t('channels.synologyChatGuide2'),
|
||||
t('channels.synologyChatGuide3'),
|
||||
t('channels.synologyChatGuide4'),
|
||||
],
|
||||
guideFooter: t('channels.synologyChatGuideFooter'),
|
||||
fields: [
|
||||
{ key: 'token', label: 'Token', placeholder: t('channels.synologyChatTokenPh'), secret: true, required: true },
|
||||
{ key: 'incomingUrl', label: 'Incoming URL', placeholder: 'https://nas.example.com/webapi/entry.cgi', required: true, hint: t('channels.synologyChatIncomingUrlHint') },
|
||||
{ key: 'nasHost', label: 'NAS Host', placeholder: 'https://nas.example.com', required: false },
|
||||
{ key: 'webhookPath', label: 'Webhook Path', placeholder: '/webhook/synology', required: false },
|
||||
{ key: 'dmPolicy', label: t('channels.dmPolicy'), type: 'select', options: SYNOLOGY_DM_POLICY_OPTIONS, required: false },
|
||||
{ key: 'allowedUserIds', label: 'Allowed User IDs', placeholder: 'alice, bob', required: false, hint: t('channels.synologyChatAllowedUserIdsHint') },
|
||||
{ key: 'rateLimitPerMinute', label: 'Rate Limit / Minute', placeholder: '30', required: false },
|
||||
{ key: 'botName', label: 'Bot Name', placeholder: 'OpenClaw', required: false },
|
||||
{ key: 'dangerouslyAllowNameMatching', label: t('channels.synologyChatNameMatching'), type: 'select', options: BOOLEAN_OPTIONS, required: false, hint: t('channels.synologyChatNameMatchingHint') },
|
||||
{ key: 'dangerouslyAllowInheritedWebhookPath', label: t('channels.synologyChatInheritedWebhookPath'), type: 'select', options: BOOLEAN_OPTIONS, required: false, hint: t('channels.synologyChatInheritedWebhookPathHint') },
|
||||
{ key: 'allowInsecureSsl', label: t('channels.synologyChatAllowInsecureSsl'), type: 'select', options: BOOLEAN_OPTIONS, required: false, hint: t('channels.synologyChatAllowInsecureSslHint') },
|
||||
],
|
||||
configKey: 'synology-chat',
|
||||
pluginRequired: '@openclaw/synology-chat@latest',
|
||||
pluginId: 'synology-chat',
|
||||
},
|
||||
googlechat: {
|
||||
label: 'Google Chat',
|
||||
iconName: 'message-square',
|
||||
desc: t('channels.googleChatDesc'),
|
||||
guide: [
|
||||
t('channels.googleChatGuide1'),
|
||||
t('channels.googleChatGuide2'),
|
||||
t('channels.googleChatGuide3'),
|
||||
t('channels.googleChatGuide4'),
|
||||
],
|
||||
guideFooter: t('channels.googleChatGuideFooter'),
|
||||
fields: [
|
||||
{ key: 'serviceAccountFile', label: 'Service Account File', placeholder: '/path/to/service-account.json', required: false, hint: t('channels.googleChatServiceAccountFileHint') },
|
||||
{ key: 'serviceAccount', label: 'Service Account JSON', placeholder: '{"type":"service_account", ...}', secret: true, multiline: true, required: false, hint: t('channels.googleChatServiceAccountHint') },
|
||||
{ key: 'serviceAccountRef', label: 'Service Account SecretRef', placeholder: 'SecretRef(env:default:GOOGLE_CHAT_SERVICE_ACCOUNT)', required: false, hint: t('channels.googleChatServiceAccountRefHint') },
|
||||
{ key: 'audienceType', label: 'Audience Type', type: 'select', options: [
|
||||
{ value: '', label: t('channels.policyDefault') },
|
||||
{ value: 'app-url', label: 'App URL' },
|
||||
{ value: 'project-number', label: 'Project Number' },
|
||||
], required: false },
|
||||
{ key: 'audience', label: 'Audience', placeholder: 'https://panel.example.com/googlechat', required: false, hint: t('channels.googleChatAudienceHint') },
|
||||
{ key: 'webhookPath', label: 'Webhook Path', placeholder: '/googlechat', required: false },
|
||||
{ key: 'webhookUrl', label: 'Webhook URL', placeholder: 'https://panel.example.com/googlechat', required: false },
|
||||
{ key: 'dmPolicy', label: t('channels.dmPolicy'), type: 'select', options: DM_POLICY_OPTIONS, required: false },
|
||||
{ key: 'groupPolicy', label: t('channels.groupPolicy'), type: 'select', options: GROUP_POLICY_OPTIONS(t('channels.groupAllSpaces'), { mention: true }), required: false },
|
||||
{ key: 'allowFrom', label: 'Allow From', placeholder: 'users/123456789, name@example.com', required: false, hint: t('channels.googleChatAllowFromHint') },
|
||||
{ key: 'groupAllowFrom', label: 'Group Allow From', placeholder: 'spaces/AAA, spaces/BBB', required: false, hint: t('channels.googleChatGroupAllowFromHint') },
|
||||
{ key: 'dangerouslyAllowNameMatching', label: t('channels.googleChatNameMatching'), type: 'select', options: BOOLEAN_OPTIONS, required: false, hint: t('channels.googleChatNameMatchingHint') },
|
||||
{ key: 'requireMention', label: t('channels.googleChatRequireMention'), type: 'select', options: BOOLEAN_OPTIONS, required: false },
|
||||
{ key: 'mediaMaxMb', label: 'Media Max MB', placeholder: '20', required: false },
|
||||
{ key: 'responsePrefix', label: 'Response Prefix', placeholder: t('channels.optionalEg', { example: '[AI]' }), required: false },
|
||||
],
|
||||
requiredAny: [{ keys: ['serviceAccountFile', 'serviceAccount', 'serviceAccountRef'], label: t('channels.googleChatServiceAccountRequired') }],
|
||||
configKey: 'googlechat',
|
||||
pairingChannel: 'googlechat',
|
||||
pluginRequired: '@openclaw/googlechat@latest',
|
||||
pluginId: 'googlechat',
|
||||
},
|
||||
discord: {
|
||||
label: 'Discord',
|
||||
iconName: 'hash',
|
||||
@@ -577,7 +650,7 @@ function applyRouteIntent(page, state) {
|
||||
// ── 已配置平台渲染 ──
|
||||
|
||||
// ── 多账号支持的平台:与 OpenClaw 的 accounts/defaultAccount 配置模型保持一致 ──
|
||||
const MULTI_INSTANCE_PLATFORMS = ['telegram', 'discord', 'slack', 'feishu', 'dingtalk', 'dingtalk-connector', 'qqbot', 'zalo', 'zalouser', 'line', 'mattermost']
|
||||
const MULTI_INSTANCE_PLATFORMS = ['telegram', 'discord', 'slack', 'feishu', 'dingtalk', 'dingtalk-connector', 'qqbot', 'zalo', 'zalouser', 'line', 'mattermost', 'synology-chat', 'googlechat']
|
||||
|
||||
function supportsMessagingMultiAccount(pid) {
|
||||
return MULTI_INSTANCE_PLATFORMS.includes(pid)
|
||||
@@ -2156,12 +2229,21 @@ async function openConfigDialog(pid, page, state, accountId) {
|
||||
</div>
|
||||
`
|
||||
}
|
||||
if (f.multiline) {
|
||||
return `
|
||||
<div class="form-group">
|
||||
<label class="form-label">${labelWithHelp(f.label)}${f.required ? ' *' : ''}</label>
|
||||
<textarea class="form-input" name="${f.key}" rows="5" placeholder="${escapeAttr(f.placeholder || '')}" ${i === 0 ? 'autofocus' : ''} style="width:100%;min-height:112px;resize:vertical;font-family:var(--font-mono);line-height:1.5">${escapeAttr(val)}</textarea>
|
||||
${fieldHint ? `<div class="form-hint">${fieldHint}</div>` : ''}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
return `
|
||||
<div class="form-group">
|
||||
<label class="form-label">${labelWithHelp(f.label)}${f.required ? ' *' : ''}</label>
|
||||
<div style="display:flex;gap:8px">
|
||||
<input class="form-input" name="${f.key}" type="${f.secret ? 'password' : 'text'}"
|
||||
value="${escapeAttr(val)}" placeholder="${f.placeholder || ''}"
|
||||
value="${escapeAttr(val)}" placeholder="${escapeAttr(f.placeholder || '')}"
|
||||
${i === 0 ? 'autofocus' : ''} style="flex:1">
|
||||
${f.secret ? `<button type="button" class="btn btn-sm btn-secondary toggle-vis" data-field="${f.key}">${t('channels.show')}</button>` : ''}
|
||||
</div>
|
||||
@@ -2284,7 +2366,7 @@ async function openConfigDialog(pid, page, state, accountId) {
|
||||
const collectForm = () => {
|
||||
const obj = {}
|
||||
reg.fields.forEach(f => {
|
||||
const el = modal.querySelector(`input[name="${f.key}"]`) || modal.querySelector(`select[name="${f.key}"]`)
|
||||
const el = modal.querySelector(`input[name="${f.key}"]`) || modal.querySelector(`select[name="${f.key}"]`) || modal.querySelector(`textarea[name="${f.key}"]`)
|
||||
if (el) obj[f.key] = el.value.trim()
|
||||
})
|
||||
return obj
|
||||
|
||||
@@ -569,6 +569,149 @@ test('Mattermost 诊断要求 Bot Token 和 Base URL', () => {
|
||||
assert.equal(ready.checks.find(item => item.id === 'credentials')?.ok, true)
|
||||
})
|
||||
|
||||
test('Synology Chat 渠道保存会写入上游运行时字段并支持多账号', () => {
|
||||
const cfg = { channels: {} }
|
||||
|
||||
mergeOpenClawMessagingPlatformConfig(cfg, {
|
||||
platform: 'synology-chat',
|
||||
accountId: 'nas',
|
||||
form: {
|
||||
token: 'synology-token',
|
||||
incomingUrl: 'https://nas.example.com/webapi/entry.cgi',
|
||||
nasHost: 'https://nas.example.com',
|
||||
webhookPath: '/webhook/synology',
|
||||
dmPolicy: 'allowlist',
|
||||
allowedUserIds: 'alice, bob',
|
||||
rateLimitPerMinute: '45',
|
||||
botName: 'OpenClaw Ops',
|
||||
dangerouslyAllowNameMatching: 'true',
|
||||
dangerouslyAllowInheritedWebhookPath: 'true',
|
||||
allowInsecureSsl: 'true',
|
||||
},
|
||||
})
|
||||
|
||||
const account = cfg.channels['synology-chat'].accounts.nas
|
||||
assert.equal(cfg.channels['synology-chat'].defaultAccount, 'nas')
|
||||
assert.equal(account.token, 'synology-token')
|
||||
assert.equal(account.incomingUrl, 'https://nas.example.com/webapi/entry.cgi')
|
||||
assert.equal(account.nasHost, 'https://nas.example.com')
|
||||
assert.equal(account.webhookPath, '/webhook/synology')
|
||||
assert.equal(account.dmPolicy, 'allowlist')
|
||||
assert.deepEqual(account.allowedUserIds, ['alice', 'bob'])
|
||||
assert.equal(account.rateLimitPerMinute, 45)
|
||||
assert.equal(account.botName, 'OpenClaw Ops')
|
||||
assert.equal(account.dangerouslyAllowNameMatching, true)
|
||||
assert.equal(account.dangerouslyAllowInheritedWebhookPath, true)
|
||||
assert.equal(account.allowInsecureSsl, true)
|
||||
assert.equal(cfg.plugins.entries['synology-chat'].enabled, true)
|
||||
})
|
||||
|
||||
test('Synology Chat 诊断要求 Token 和 Incoming URL', () => {
|
||||
const missingUrl = buildOpenClawChannelDiagnosis({
|
||||
platform: 'synology-chat',
|
||||
configExists: true,
|
||||
channelEnabled: true,
|
||||
form: { token: 'synology-token' },
|
||||
})
|
||||
const ready = buildOpenClawChannelDiagnosis({
|
||||
platform: 'synology-chat',
|
||||
configExists: true,
|
||||
channelEnabled: true,
|
||||
form: {
|
||||
token: 'synology-token',
|
||||
incomingUrl: 'https://nas.example.com/webapi/entry.cgi',
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(missingUrl.checks.find(item => item.id === 'credentials')?.ok, false)
|
||||
assert.match(missingUrl.checks.find(item => item.id === 'credentials')?.detail || '', /Incoming URL/)
|
||||
assert.equal(ready.checks.find(item => item.id === 'credentials')?.ok, true)
|
||||
})
|
||||
|
||||
test('Google Chat 渠道保存会写入 service account 与嵌套 DM 策略', () => {
|
||||
const cfg = { channels: {} }
|
||||
|
||||
mergeOpenClawMessagingPlatformConfig(cfg, {
|
||||
platform: 'googlechat',
|
||||
accountId: 'workspace',
|
||||
form: {
|
||||
serviceAccountFile: '/run/secrets/googlechat.json',
|
||||
audienceType: 'app-url',
|
||||
audience: 'https://panel.example.com/googlechat',
|
||||
webhookPath: '/googlechat',
|
||||
webhookUrl: 'https://panel.example.com/googlechat',
|
||||
dmPolicy: 'open',
|
||||
allowFrom: 'users/123',
|
||||
groupPolicy: 'mentioned',
|
||||
groupAllowFrom: 'spaces/AAA',
|
||||
dangerouslyAllowNameMatching: 'true',
|
||||
requireMention: 'true',
|
||||
mediaMaxMb: '20',
|
||||
responsePrefix: '[AI]',
|
||||
},
|
||||
})
|
||||
|
||||
const account = cfg.channels.googlechat.accounts.workspace
|
||||
assert.equal(cfg.channels.googlechat.defaultAccount, 'workspace')
|
||||
assert.equal(account.serviceAccountFile, '/run/secrets/googlechat.json')
|
||||
assert.equal(account.audienceType, 'app-url')
|
||||
assert.equal(account.audience, 'https://panel.example.com/googlechat')
|
||||
assert.equal(account.webhookPath, '/googlechat')
|
||||
assert.equal(account.webhookUrl, 'https://panel.example.com/googlechat')
|
||||
assert.deepEqual(account.dm, { policy: 'open', allowFrom: ['users/123', '*'] })
|
||||
assert.equal(account.groupPolicy, 'open')
|
||||
assert.equal(account.requireMention, true)
|
||||
assert.deepEqual(account.groupAllowFrom, ['spaces/AAA'])
|
||||
assert.equal(account.dangerouslyAllowNameMatching, true)
|
||||
assert.equal(account.mediaMaxMb, 20)
|
||||
assert.equal(account.responsePrefix, '[AI]')
|
||||
assert.equal(cfg.plugins.entries.googlechat.enabled, true)
|
||||
})
|
||||
|
||||
test('Google Chat 读取会把嵌套 DM 策略回显为表单字段', () => {
|
||||
const values = buildMessagingPlatformFormValues('googlechat', {
|
||||
serviceAccountFile: '/run/secrets/googlechat.json',
|
||||
audienceType: 'project-number',
|
||||
audience: '1234567890',
|
||||
dm: { policy: 'allowlist', allowFrom: ['users/123', 'name@example.com'] },
|
||||
groupPolicy: 'allowlist',
|
||||
groupAllowFrom: ['spaces/AAA'],
|
||||
requireMention: true,
|
||||
dangerouslyAllowNameMatching: true,
|
||||
mediaMaxMb: 20,
|
||||
})
|
||||
|
||||
assert.equal(values.serviceAccountFile, '/run/secrets/googlechat.json')
|
||||
assert.equal(values.audienceType, 'project-number')
|
||||
assert.equal(values.audience, '1234567890')
|
||||
assert.equal(values.dmPolicy, 'allowlist')
|
||||
assert.equal(values.allowFrom, 'users/123, name@example.com')
|
||||
assert.equal(values.groupPolicy, 'allowlist')
|
||||
assert.equal(values.groupAllowFrom, 'spaces/AAA')
|
||||
assert.equal(values.requireMention, 'true')
|
||||
assert.equal(values.dangerouslyAllowNameMatching, 'true')
|
||||
assert.equal(values.mediaMaxMb, '20')
|
||||
})
|
||||
|
||||
test('Google Chat 诊断要求 service account 文件或内联 JSON 其中一项', () => {
|
||||
const missingCredential = buildOpenClawChannelDiagnosis({
|
||||
platform: 'googlechat',
|
||||
configExists: true,
|
||||
channelEnabled: true,
|
||||
form: { audienceType: 'app-url', audience: 'https://panel.example.com/googlechat' },
|
||||
})
|
||||
const ready = buildOpenClawChannelDiagnosis({
|
||||
platform: 'googlechat',
|
||||
configExists: true,
|
||||
channelEnabled: true,
|
||||
form: { serviceAccountFile: '/run/secrets/googlechat.json' },
|
||||
})
|
||||
|
||||
assert.equal(missingCredential.checks.find(item => item.id === 'credentials')?.ok, false)
|
||||
assert.match(missingCredential.checks.find(item => item.id === 'credentials')?.detail || '', /Service Account/)
|
||||
assert.equal(ready.checks.find(item => item.id === 'credentials')?.ok, true)
|
||||
})
|
||||
|
||||
test('Discord 渠道保存会保留运行时需要的 applicationId', () => {
|
||||
const cfg = { channels: {} }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user