mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 04:10:00 +08:00
fix(channels): normalize OpenClaw channel config policies
This commit is contained in:
@@ -2386,6 +2386,219 @@ function platformBindingChannel(platform) {
|
||||
return platformListId(storageKey)
|
||||
}
|
||||
|
||||
function csvToStringArray(raw) {
|
||||
if (Array.isArray(raw)) return raw.map(item => String(item).trim()).filter(Boolean)
|
||||
if (typeof raw !== 'string') return []
|
||||
return raw.split(/[,;\n]/).map(item => item.trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
function normalizeDmPolicy(raw, fallback = 'pairing') {
|
||||
const value = String(raw || '').trim()
|
||||
if (!value) return fallback
|
||||
if (value === 'allow') return 'open'
|
||||
if (value === 'deny') return 'disabled'
|
||||
if (['pairing', 'allowlist', 'open', 'disabled'].includes(value)) return value
|
||||
return fallback
|
||||
}
|
||||
|
||||
function normalizeGroupPolicy(raw, fallback = 'allowlist') {
|
||||
const value = String(raw || '').trim()
|
||||
if (!value) return fallback
|
||||
if (value === 'all') return 'open'
|
||||
if (value === 'mentioned') return 'open'
|
||||
if (value === 'deny') return 'disabled'
|
||||
if (['open', 'allowlist', 'disabled'].includes(value)) return value
|
||||
return fallback
|
||||
}
|
||||
|
||||
function putWildcardAllowFromWhenOpen(entry, previousAllowFrom) {
|
||||
if (entry.dmPolicy !== 'open') return
|
||||
const allowFrom = csvToStringArray(previousAllowFrom)
|
||||
if (!allowFrom.includes('*')) allowFrom.push('*')
|
||||
entry.allowFrom = allowFrom
|
||||
}
|
||||
|
||||
function platformSupportsTopLevelRequireMention(platform) {
|
||||
return ['feishu', 'slack', 'msteams'].includes(platformStorageKey(platform))
|
||||
}
|
||||
|
||||
export function normalizeMessagingPlatformForm(platform, form = {}) {
|
||||
const storageKey = platformStorageKey(platform)
|
||||
const normalized = { ...(form || {}) }
|
||||
if (!Object.hasOwn(normalized, 'allowFrom') && Object.hasOwn(normalized, 'allowedUsers')) {
|
||||
normalized.allowFrom = normalized.allowedUsers
|
||||
}
|
||||
const needsAccessDefaults = ['telegram', 'discord', 'feishu', 'slack', 'signal', 'msteams', 'whatsapp'].includes(storageKey)
|
||||
const hasDmField = Object.hasOwn(normalized, 'dmPolicy') || needsAccessDefaults
|
||||
const hasGroupField = Object.hasOwn(normalized, 'groupPolicy') || needsAccessDefaults
|
||||
|
||||
if (hasDmField) {
|
||||
normalized.dmPolicy = normalizeDmPolicy(normalized.dmPolicy)
|
||||
if (Object.hasOwn(normalized, 'allowFrom')) normalized.allowFrom = csvToStringArray(normalized.allowFrom)
|
||||
putWildcardAllowFromWhenOpen(normalized, normalized.allowFrom)
|
||||
} else if (Object.hasOwn(normalized, 'allowFrom')) {
|
||||
normalized.allowFrom = csvToStringArray(normalized.allowFrom)
|
||||
}
|
||||
|
||||
if (hasGroupField) {
|
||||
const requestedGroupPolicy = String(normalized.groupPolicy || '').trim()
|
||||
normalized.groupPolicy = normalizeGroupPolicy(requestedGroupPolicy)
|
||||
if (requestedGroupPolicy === 'mentioned' && platformSupportsTopLevelRequireMention(storageKey)) {
|
||||
normalized.requireMention = true
|
||||
} else if (requestedGroupPolicy !== 'mentioned') {
|
||||
if (platformSupportsTopLevelRequireMention(storageKey)) {
|
||||
normalized.requireMention = false
|
||||
} else if (Object.hasOwn(normalized, 'requireMention')) {
|
||||
normalized.requireMention = normalized.requireMention === true || normalized.requireMention === 'true'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (storageKey === 'feishu') {
|
||||
normalized.domain = String(normalized.domain || '').trim() || 'feishu'
|
||||
normalized.connectionMode = normalized.connectionMode || 'websocket'
|
||||
normalized.webhookPath = normalized.webhookPath || '/feishu/events'
|
||||
normalized.reactionNotifications = normalized.reactionNotifications || 'off'
|
||||
if (!Object.hasOwn(normalized, 'typingIndicator')) normalized.typingIndicator = true
|
||||
if (!Object.hasOwn(normalized, 'resolveSenderNames')) normalized.resolveSenderNames = true
|
||||
}
|
||||
|
||||
if (storageKey === 'slack') {
|
||||
normalized.mode = normalized.mode || 'socket'
|
||||
normalized.webhookPath = normalized.webhookPath || '/slack/events'
|
||||
if (!Object.hasOwn(normalized, 'userTokenReadOnly')) normalized.userTokenReadOnly = false
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
function csvForForm(raw) {
|
||||
return csvToStringArray(raw).join(', ')
|
||||
}
|
||||
|
||||
function putStringFormValue(form, source, key) {
|
||||
if (typeof source?.[key] === 'string') form[key] = source[key]
|
||||
}
|
||||
|
||||
function putBoolFormValue(form, source, key) {
|
||||
if (typeof source?.[key] === 'boolean') form[key] = source[key] ? 'true' : 'false'
|
||||
}
|
||||
|
||||
function putCsvFormValue(form, source, key) {
|
||||
const value = csvForForm(source?.[key])
|
||||
if (value) form[key] = value
|
||||
}
|
||||
|
||||
function putAccessPolicyFormValues(form, source, { telegramCompat = false, mentionCompat = false } = {}) {
|
||||
putStringFormValue(form, source, 'dmPolicy')
|
||||
putStringFormValue(form, source, 'groupPolicy')
|
||||
if (mentionCompat && form.groupPolicy === 'open' && source?.requireMention === true) {
|
||||
form.groupPolicy = 'mentioned'
|
||||
}
|
||||
putCsvFormValue(form, source, 'allowFrom')
|
||||
if (telegramCompat && form.allowFrom) form.allowedUsers = form.allowFrom
|
||||
}
|
||||
|
||||
export function buildMessagingPlatformFormValues(platform, saved = {}, options = {}) {
|
||||
if (!saved || typeof saved !== 'object') return {}
|
||||
const form = {}
|
||||
const storageKey = platformStorageKey(platform)
|
||||
|
||||
if (storageKey === 'telegram') {
|
||||
putStringFormValue(form, saved, 'botToken')
|
||||
putAccessPolicyFormValues(form, saved, { telegramCompat: true })
|
||||
return form
|
||||
}
|
||||
|
||||
if (storageKey === 'discord') {
|
||||
putStringFormValue(form, saved, 'token')
|
||||
putAccessPolicyFormValues(form, saved)
|
||||
const guilds = saved.guilds && typeof saved.guilds === 'object' ? saved.guilds : null
|
||||
const guildId = guilds ? Object.keys(guilds)[0] : ''
|
||||
if (guildId) {
|
||||
form.guildId = guildId
|
||||
const channels = guilds[guildId]?.channels && typeof guilds[guildId].channels === 'object'
|
||||
? guilds[guildId].channels
|
||||
: null
|
||||
const channelId = channels ? Object.keys(channels).find(id => id !== '*') : ''
|
||||
if (channelId) form.channelId = channelId
|
||||
}
|
||||
return form
|
||||
}
|
||||
|
||||
if (storageKey === 'feishu') {
|
||||
putStringFormValue(form, saved, 'appId')
|
||||
putStringFormValue(form, saved, 'appSecret')
|
||||
const shared = options.channelRoot && typeof options.channelRoot === 'object'
|
||||
? { ...saved, ...options.channelRoot }
|
||||
: saved
|
||||
for (const key of ['domain', 'connectionMode', 'webhookPath', 'reactionNotifications', 'textChunkLimit', 'mediaMaxMb']) {
|
||||
putStringFormValue(form, shared, key)
|
||||
}
|
||||
putAccessPolicyFormValues(form, shared, { mentionCompat: true })
|
||||
putBoolFormValue(form, shared, 'typingIndicator')
|
||||
putBoolFormValue(form, shared, 'resolveSenderNames')
|
||||
putBoolFormValue(form, shared, 'requireMention')
|
||||
return form
|
||||
}
|
||||
|
||||
if (storageKey === 'slack') {
|
||||
for (const key of ['mode', 'botToken', 'appToken', 'signingSecret', 'webhookPath', 'teamId', 'appId', 'socketMode']) {
|
||||
putStringFormValue(form, saved, key)
|
||||
}
|
||||
putAccessPolicyFormValues(form, saved, { mentionCompat: true })
|
||||
putBoolFormValue(form, saved, 'userTokenReadOnly')
|
||||
putBoolFormValue(form, saved, 'requireMention')
|
||||
return form
|
||||
}
|
||||
|
||||
if (storageKey === 'whatsapp') {
|
||||
putAccessPolicyFormValues(form, saved, { mentionCompat: true })
|
||||
putBoolFormValue(form, saved, 'enabled')
|
||||
return form
|
||||
}
|
||||
|
||||
if (storageKey === 'signal') {
|
||||
for (const key of ['account', 'cliPath', 'httpUrl', 'httpHost', 'httpPort']) {
|
||||
putStringFormValue(form, saved, key)
|
||||
}
|
||||
putAccessPolicyFormValues(form, saved)
|
||||
return form
|
||||
}
|
||||
|
||||
if (storageKey === 'matrix') {
|
||||
for (const key of ['homeserver', 'accessToken', 'userId', 'password', 'deviceId']) {
|
||||
putStringFormValue(form, saved, key)
|
||||
}
|
||||
putAccessPolicyFormValues(form, saved)
|
||||
putBoolFormValue(form, saved, 'e2ee')
|
||||
if (form.accessToken) form.authMode = 'token'
|
||||
else if (form.userId || form.password) form.authMode = 'password'
|
||||
return form
|
||||
}
|
||||
|
||||
if (storageKey === 'msteams') {
|
||||
for (const key of ['appId', 'appPassword', 'tenantId', 'botEndpoint', 'webhookPath']) {
|
||||
putStringFormValue(form, saved, key)
|
||||
}
|
||||
putAccessPolicyFormValues(form, saved)
|
||||
putBoolFormValue(form, saved, 'requireMention')
|
||||
return form
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(saved)) {
|
||||
if (key === 'enabled' || key === 'accounts') continue
|
||||
if (typeof value === 'string') form[key] = value
|
||||
else if (Array.isArray(value)) {
|
||||
const csv = csvForForm(value)
|
||||
if (csv) form[key] = csv
|
||||
} else if (typeof value === 'boolean') {
|
||||
form[key] = value ? 'true' : 'false'
|
||||
}
|
||||
}
|
||||
return form
|
||||
}
|
||||
|
||||
function channelHasQqbotCredentials(entry) {
|
||||
return !!(entry && typeof entry === 'object' && (entry.appId || entry.clientSecret || entry.appSecret || entry.token))
|
||||
}
|
||||
@@ -3872,21 +4085,8 @@ const handlers = {
|
||||
if (!appId && !clientSecret) return { exists: false }
|
||||
if (appId) form.appId = appId
|
||||
if (clientSecret) form.clientSecret = clientSecret
|
||||
} else if (platform === 'telegram') {
|
||||
if (saved.botToken) form.botToken = saved.botToken
|
||||
if (saved.allowFrom) form.allowedUsers = saved.allowFrom.join(', ')
|
||||
} else if (platform === 'discord') {
|
||||
if (saved.token) form.token = saved.token
|
||||
const gid = saved.guilds && Object.keys(saved.guilds)[0]
|
||||
if (gid) form.guildId = gid
|
||||
} else if (platform === 'feishu') {
|
||||
if (saved.appId) form.appId = saved.appId
|
||||
if (saved.appSecret) form.appSecret = saved.appSecret
|
||||
if (saved.domain) form.domain = saved.domain
|
||||
} else {
|
||||
for (const [k, v] of Object.entries(saved)) {
|
||||
if (k !== 'enabled' && k !== 'accounts' && typeof v === 'string') form[k] = v
|
||||
}
|
||||
Object.assign(form, buildMessagingPlatformFormValues(platform, saved, { channelRoot }))
|
||||
}
|
||||
return { exists: true, values: form }
|
||||
},
|
||||
@@ -3894,6 +4094,7 @@ const handlers = {
|
||||
save_messaging_platform({ platform, form, accountId }) {
|
||||
if (!fs.existsSync(CONFIG_PATH)) throw new Error('openclaw.json 不存在')
|
||||
const cfg = readOpenclawConfigRequired()
|
||||
form = normalizeMessagingPlatformForm(platform, form || {})
|
||||
if (!cfg.channels) cfg.channels = {}
|
||||
const storageKey = platformStorageKey(platform)
|
||||
const normalizedAccountId = typeof accountId === 'string' ? accountId.trim() : ''
|
||||
@@ -3936,9 +4137,14 @@ const handlers = {
|
||||
cfg.channels.qqbot = current
|
||||
} else if (platform === 'telegram') {
|
||||
entry.botToken = form.botToken
|
||||
if (form.allowedUsers) entry.allowFrom = form.allowedUsers.split(',').map(s => s.trim()).filter(Boolean)
|
||||
entry.dmPolicy = form.dmPolicy
|
||||
entry.groupPolicy = form.groupPolicy
|
||||
if (Array.isArray(form.allowFrom) && form.allowFrom.length) entry.allowFrom = form.allowFrom
|
||||
} else if (platform === 'discord') {
|
||||
entry.token = form.token
|
||||
entry.dmPolicy = form.dmPolicy
|
||||
entry.groupPolicy = form.groupPolicy
|
||||
if (Array.isArray(form.allowFrom) && form.allowFrom.length) entry.allowFrom = form.allowFrom
|
||||
if (form.guildId) {
|
||||
const ck = form.channelId || '*'
|
||||
entry.guilds = { [form.guildId]: { users: ['*'], requireMention: true, channels: { [ck]: { allow: true, requireMention: true } } } }
|
||||
@@ -3947,7 +4153,15 @@ const handlers = {
|
||||
entry.appId = form.appId
|
||||
entry.appSecret = form.appSecret
|
||||
entry.connectionMode = 'websocket'
|
||||
if (form.domain) entry.domain = form.domain
|
||||
entry.domain = form.domain
|
||||
entry.webhookPath = form.webhookPath
|
||||
entry.dmPolicy = form.dmPolicy
|
||||
entry.groupPolicy = form.groupPolicy
|
||||
if (Array.isArray(form.allowFrom) && form.allowFrom.length) entry.allowFrom = form.allowFrom
|
||||
if (Object.hasOwn(form, 'requireMention')) entry.requireMention = !!form.requireMention
|
||||
entry.reactionNotifications = form.reactionNotifications
|
||||
entry.typingIndicator = form.typingIndicator
|
||||
entry.resolveSenderNames = form.resolveSenderNames
|
||||
if (normalizedAccountId) {
|
||||
setAccountChannelEntry(entry)
|
||||
} else {
|
||||
|
||||
@@ -80,6 +80,28 @@ fn insert_array_as_csv(form: &mut Map<String, Value>, source: &Value, key: &str)
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_access_policy_form_values(
|
||||
form: &mut Map<String, Value>,
|
||||
source: &Value,
|
||||
telegram_compat: bool,
|
||||
mention_compat: bool,
|
||||
) {
|
||||
insert_string_if_present(form, source, "dmPolicy");
|
||||
insert_string_if_present(form, source, "groupPolicy");
|
||||
if mention_compat
|
||||
&& form.get("groupPolicy").and_then(|v| v.as_str()) == Some("open")
|
||||
&& source.get("requireMention").and_then(|v| v.as_bool()) == Some(true)
|
||||
{
|
||||
form.insert("groupPolicy".into(), Value::String("mentioned".into()));
|
||||
}
|
||||
insert_array_as_csv(form, source, "allowFrom");
|
||||
if telegram_compat {
|
||||
if let Some(v) = form.get("allowFrom").cloned() {
|
||||
form.insert("allowedUsers".into(), v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn csv_to_json_array(raw: &str) -> Option<Value> {
|
||||
let items = raw
|
||||
.split(&[',', '\n', ';'][..])
|
||||
@@ -94,6 +116,32 @@ fn csv_to_json_array(raw: &str) -> Option<Value> {
|
||||
}
|
||||
}
|
||||
|
||||
fn json_array_from_csv_value(value: Option<&Value>) -> Vec<Value> {
|
||||
match value {
|
||||
Some(Value::Array(items)) => items
|
||||
.iter()
|
||||
.filter_map(|v| {
|
||||
if let Some(s) = v.as_str() {
|
||||
let trimmed = s.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Value::String(trimmed.to_string()))
|
||||
}
|
||||
} else if v.is_number() || v.is_boolean() {
|
||||
Some(Value::String(v.to_string()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
Some(Value::String(raw)) => csv_to_json_array(raw)
|
||||
.and_then(|v| v.as_array().cloned())
|
||||
.unwrap_or_default(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn bool_from_form_value(raw: &str) -> Option<bool> {
|
||||
match raw.trim().to_ascii_lowercase().as_str() {
|
||||
"true" | "1" | "yes" | "on" => Some(true),
|
||||
@@ -114,12 +162,161 @@ fn put_bool_from_form(entry: &mut Map<String, Value>, key: &str, raw: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
fn put_csv_array_from_form(entry: &mut Map<String, Value>, key: &str, raw: &str) {
|
||||
if let Some(v) = csv_to_json_array(raw) {
|
||||
entry.insert(key.into(), v);
|
||||
fn put_bool_value_if_present(entry: &mut Map<String, Value>, key: &str, value: Option<&Value>) {
|
||||
match value {
|
||||
Some(Value::Bool(v)) => {
|
||||
entry.insert(key.into(), Value::Bool(*v));
|
||||
}
|
||||
Some(Value::String(raw)) => put_bool_from_form(entry, key, raw),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn put_array_from_form_value(entry: &mut Map<String, Value>, key: &str, value: Option<&Value>) {
|
||||
let items = json_array_from_csv_value(value);
|
||||
if !items.is_empty() {
|
||||
entry.insert(key.into(), Value::Array(items));
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_dm_policy_value(raw: Option<&Value>, fallback: &str) -> String {
|
||||
let value = raw.and_then(|v| v.as_str()).unwrap_or("").trim();
|
||||
match value {
|
||||
"" => fallback.to_string(),
|
||||
"allow" | "open" => "open".into(),
|
||||
"deny" | "disabled" => "disabled".into(),
|
||||
"pairing" => "pairing".into(),
|
||||
"allowlist" => "allowlist".into(),
|
||||
_ => fallback.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_group_policy_value(raw: Option<&Value>, fallback: &str) -> String {
|
||||
let value = raw.and_then(|v| v.as_str()).unwrap_or("").trim();
|
||||
match value {
|
||||
"" => fallback.to_string(),
|
||||
"all" | "mentioned" | "open" => "open".into(),
|
||||
"deny" | "disabled" => "disabled".into(),
|
||||
"allowlist" => "allowlist".into(),
|
||||
_ => fallback.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn platform_supports_top_level_require_mention(platform: &str) -> bool {
|
||||
matches!(
|
||||
platform_storage_key(platform),
|
||||
"feishu" | "slack" | "msteams"
|
||||
)
|
||||
}
|
||||
|
||||
fn normalize_messaging_platform_form(
|
||||
platform: &str,
|
||||
form: &Map<String, Value>,
|
||||
) -> Map<String, Value> {
|
||||
let storage_key = platform_storage_key(platform);
|
||||
let mut normalized = form.clone();
|
||||
|
||||
if !normalized.contains_key("allowFrom") {
|
||||
if let Some(v) = normalized.get("allowedUsers").cloned() {
|
||||
normalized.insert("allowFrom".into(), v);
|
||||
}
|
||||
}
|
||||
|
||||
let needs_access_defaults = matches!(
|
||||
storage_key,
|
||||
"telegram" | "discord" | "feishu" | "slack" | "signal" | "msteams" | "whatsapp"
|
||||
);
|
||||
let has_dm_field = normalized.contains_key("dmPolicy") || needs_access_defaults;
|
||||
let has_group_field = normalized.contains_key("groupPolicy") || needs_access_defaults;
|
||||
|
||||
if has_dm_field {
|
||||
let dm_policy = normalize_dm_policy_value(normalized.get("dmPolicy"), "pairing");
|
||||
normalized.insert("dmPolicy".into(), Value::String(dm_policy.clone()));
|
||||
if normalized.contains_key("allowFrom") {
|
||||
let items = json_array_from_csv_value(normalized.get("allowFrom"));
|
||||
normalized.insert("allowFrom".into(), Value::Array(items));
|
||||
}
|
||||
if dm_policy == "open" {
|
||||
let mut items = json_array_from_csv_value(normalized.get("allowFrom"));
|
||||
if !items.iter().any(|v| v.as_str() == Some("*")) {
|
||||
items.push(Value::String("*".into()));
|
||||
}
|
||||
normalized.insert("allowFrom".into(), Value::Array(items));
|
||||
}
|
||||
} else if normalized.contains_key("allowFrom") {
|
||||
let items = json_array_from_csv_value(normalized.get("allowFrom"));
|
||||
normalized.insert("allowFrom".into(), Value::Array(items));
|
||||
}
|
||||
|
||||
if has_group_field {
|
||||
let requested_group_policy = normalized
|
||||
.get("groupPolicy")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_string();
|
||||
let group_policy = normalize_group_policy_value(normalized.get("groupPolicy"), "allowlist");
|
||||
normalized.insert("groupPolicy".into(), Value::String(group_policy));
|
||||
if requested_group_policy == "mentioned"
|
||||
&& platform_supports_top_level_require_mention(storage_key)
|
||||
{
|
||||
normalized.insert("requireMention".into(), Value::Bool(true));
|
||||
} else if requested_group_policy != "mentioned" {
|
||||
if platform_supports_top_level_require_mention(storage_key) {
|
||||
normalized.insert("requireMention".into(), Value::Bool(false));
|
||||
} else if normalized.contains_key("requireMention") {
|
||||
let value = match normalized.get("requireMention") {
|
||||
Some(Value::Bool(v)) => *v,
|
||||
Some(Value::String(s)) => bool_from_form_value(s).unwrap_or(false),
|
||||
_ => false,
|
||||
};
|
||||
normalized.insert("requireMention".into(), Value::Bool(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if storage_key == "feishu" {
|
||||
let domain = normalized
|
||||
.get("domain")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.trim();
|
||||
normalized.insert(
|
||||
"domain".into(),
|
||||
Value::String(if domain.is_empty() { "feishu" } else { domain }.into()),
|
||||
);
|
||||
normalized
|
||||
.entry("connectionMode")
|
||||
.or_insert(Value::String("websocket".into()));
|
||||
normalized
|
||||
.entry("webhookPath")
|
||||
.or_insert(Value::String("/feishu/events".into()));
|
||||
normalized
|
||||
.entry("reactionNotifications")
|
||||
.or_insert(Value::String("off".into()));
|
||||
normalized
|
||||
.entry("typingIndicator")
|
||||
.or_insert(Value::Bool(true));
|
||||
normalized
|
||||
.entry("resolveSenderNames")
|
||||
.or_insert(Value::Bool(true));
|
||||
}
|
||||
|
||||
if storage_key == "slack" {
|
||||
normalized
|
||||
.entry("mode")
|
||||
.or_insert(Value::String("socket".into()));
|
||||
normalized
|
||||
.entry("webhookPath")
|
||||
.or_insert(Value::String("/slack/events".into()));
|
||||
normalized
|
||||
.entry("userTokenReadOnly")
|
||||
.or_insert(Value::Bool(false));
|
||||
}
|
||||
|
||||
normalized
|
||||
}
|
||||
|
||||
/// 合并渠道配置:将新的表单字段覆盖到现有配置上,保留用户通过 CLI 或手动编辑的自定义字段。
|
||||
/// 例如用户手动添加的 streaming / retry / dmPolicy 等不会被丢弃。
|
||||
fn merge_channel_entry(
|
||||
@@ -327,6 +524,7 @@ pub async fn read_platform_config(
|
||||
if let Some(t) = saved.get("token").and_then(|v| v.as_str()) {
|
||||
form.insert("token".into(), Value::String(t.into()));
|
||||
}
|
||||
insert_access_policy_form_values(&mut form, &saved, false, false);
|
||||
if let Some(guilds) = saved.get("guilds").and_then(|v| v.as_object()) {
|
||||
if let Some(gid) = guilds.keys().next() {
|
||||
form.insert("guildId".into(), Value::String(gid.clone()));
|
||||
@@ -349,10 +547,7 @@ pub async fn read_platform_config(
|
||||
if let Some(t) = saved.get("botToken").and_then(|v| v.as_str()) {
|
||||
form.insert("botToken".into(), Value::String(t.into()));
|
||||
}
|
||||
if let Some(arr) = saved.get("allowFrom").and_then(|v| v.as_array()) {
|
||||
let users: Vec<&str> = arr.iter().filter_map(|v| v.as_str()).collect();
|
||||
form.insert("allowedUsers".into(), Value::String(users.join(", ")));
|
||||
}
|
||||
insert_access_policy_form_values(&mut form, &saved, true, false);
|
||||
}
|
||||
"qqbot" => {
|
||||
// 多账号:读 accounts.<account_id>;单账号:先读 qqbot 根节点,若无凭证再读 accounts.default(与官方 CLI 一致)
|
||||
@@ -475,34 +670,72 @@ pub async fn read_platform_config(
|
||||
if let Some(ref acct) = account_id {
|
||||
if !acct.is_empty() {
|
||||
// 从 channel root 补 shared fields
|
||||
let mut shared_source = saved.clone();
|
||||
if let Some(ch_root) = channel_root {
|
||||
if let (Some(target), Some(root)) =
|
||||
(shared_source.as_object_mut(), ch_root.as_object())
|
||||
{
|
||||
for key in &[
|
||||
"domain",
|
||||
"connectionMode",
|
||||
"webhookPath",
|
||||
"dmPolicy",
|
||||
"groupPolicy",
|
||||
"allowFrom",
|
||||
"reactionNotifications",
|
||||
"typingIndicator",
|
||||
"resolveSenderNames",
|
||||
"requireMention",
|
||||
"textChunkLimit",
|
||||
"mediaMaxMb",
|
||||
] {
|
||||
if let Some(v) = root.get(*key) {
|
||||
target.insert(key.to_string(), v.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
for key in &[
|
||||
"domain",
|
||||
"connectionMode",
|
||||
"dmPolicy",
|
||||
"groupPolicy",
|
||||
"webhookPath",
|
||||
"groupAllowFrom",
|
||||
"groups",
|
||||
"reactionNotifications",
|
||||
"streaming",
|
||||
"blockStreaming",
|
||||
"typingIndicator",
|
||||
"resolveSenderNames",
|
||||
"textChunkLimit",
|
||||
"mediaMaxMb",
|
||||
] {
|
||||
if let Some(v) = ch_root.get(*key) {
|
||||
if let Some(v) = shared_source.get(*key) {
|
||||
if !v.is_null() {
|
||||
form.insert(key.to_string(), v.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
insert_access_policy_form_values(&mut form, &shared_source, false, true);
|
||||
insert_bool_as_string(&mut form, &shared_source, "typingIndicator");
|
||||
insert_bool_as_string(&mut form, &shared_source, "resolveSenderNames");
|
||||
insert_bool_as_string(&mut form, &shared_source, "requireMention");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 无账号:直接从 root 读 shared fields
|
||||
if let Some(v) = saved.get("domain").and_then(|v| v.as_str()) {
|
||||
form.insert("domain".into(), Value::String(v.into()));
|
||||
for key in &[
|
||||
"domain",
|
||||
"connectionMode",
|
||||
"webhookPath",
|
||||
"reactionNotifications",
|
||||
"textChunkLimit",
|
||||
"mediaMaxMb",
|
||||
] {
|
||||
insert_string_if_present(&mut form, &saved, key);
|
||||
}
|
||||
insert_access_policy_form_values(&mut form, &saved, false, true);
|
||||
insert_bool_as_string(&mut form, &saved, "typingIndicator");
|
||||
insert_bool_as_string(&mut form, &saved, "resolveSenderNames");
|
||||
insert_bool_as_string(&mut form, &saved, "requireMention");
|
||||
}
|
||||
}
|
||||
"dingtalk" | "dingtalk-connector" => {
|
||||
@@ -543,14 +776,12 @@ pub async fn read_platform_config(
|
||||
insert_string_if_present(&mut form, &saved, "teamId");
|
||||
insert_string_if_present(&mut form, &saved, "appId");
|
||||
insert_string_if_present(&mut form, &saved, "socketMode");
|
||||
insert_string_if_present(&mut form, &saved, "dmPolicy");
|
||||
insert_string_if_present(&mut form, &saved, "groupPolicy");
|
||||
insert_array_as_csv(&mut form, &saved, "allowFrom");
|
||||
insert_access_policy_form_values(&mut form, &saved, false, true);
|
||||
insert_bool_as_string(&mut form, &saved, "userTokenReadOnly");
|
||||
insert_bool_as_string(&mut form, &saved, "requireMention");
|
||||
}
|
||||
"whatsapp" => {
|
||||
insert_string_if_present(&mut form, &saved, "dmPolicy");
|
||||
insert_string_if_present(&mut form, &saved, "groupPolicy");
|
||||
insert_array_as_csv(&mut form, &saved, "allowFrom");
|
||||
insert_access_policy_form_values(&mut form, &saved, false, false);
|
||||
insert_bool_as_string(&mut form, &saved, "enabled");
|
||||
}
|
||||
"signal" => {
|
||||
@@ -559,9 +790,7 @@ pub async fn read_platform_config(
|
||||
insert_string_if_present(&mut form, &saved, "httpUrl");
|
||||
insert_string_if_present(&mut form, &saved, "httpHost");
|
||||
insert_string_if_present(&mut form, &saved, "httpPort");
|
||||
insert_string_if_present(&mut form, &saved, "dmPolicy");
|
||||
insert_string_if_present(&mut form, &saved, "groupPolicy");
|
||||
insert_array_as_csv(&mut form, &saved, "allowFrom");
|
||||
insert_access_policy_form_values(&mut form, &saved, false, false);
|
||||
}
|
||||
"matrix" => {
|
||||
insert_string_if_present(&mut form, &saved, "homeserver");
|
||||
@@ -569,10 +798,8 @@ pub async fn read_platform_config(
|
||||
insert_string_if_present(&mut form, &saved, "userId");
|
||||
insert_string_if_present(&mut form, &saved, "password");
|
||||
insert_string_if_present(&mut form, &saved, "deviceId");
|
||||
insert_string_if_present(&mut form, &saved, "dmPolicy");
|
||||
insert_string_if_present(&mut form, &saved, "groupPolicy");
|
||||
insert_access_policy_form_values(&mut form, &saved, false, false);
|
||||
insert_bool_as_string(&mut form, &saved, "e2ee");
|
||||
insert_array_as_csv(&mut form, &saved, "allowFrom");
|
||||
if saved.get("accessToken").and_then(|v| v.as_str()).is_some() {
|
||||
form.insert("authMode".into(), Value::String("token".into()));
|
||||
} else if saved.get("userId").and_then(|v| v.as_str()).is_some()
|
||||
@@ -587,9 +814,8 @@ pub async fn read_platform_config(
|
||||
insert_string_if_present(&mut form, &saved, "tenantId");
|
||||
insert_string_if_present(&mut form, &saved, "botEndpoint");
|
||||
insert_string_if_present(&mut form, &saved, "webhookPath");
|
||||
insert_string_if_present(&mut form, &saved, "dmPolicy");
|
||||
insert_string_if_present(&mut form, &saved, "groupPolicy");
|
||||
insert_array_as_csv(&mut form, &saved, "allowFrom");
|
||||
insert_access_policy_form_values(&mut form, &saved, false, true);
|
||||
insert_bool_as_string(&mut form, &saved, "requireMention");
|
||||
}
|
||||
_ => {
|
||||
if saved.is_null() {
|
||||
@@ -641,7 +867,9 @@ pub async fn save_messaging_platform(
|
||||
.or_insert_with(|| json!({}));
|
||||
let channels_map = channels.as_object_mut().ok_or("channels 节点格式错误")?;
|
||||
|
||||
let form_obj = form.as_object().ok_or("表单数据格式错误")?;
|
||||
let raw_form_obj = form.as_object().ok_or("表单数据格式错误")?;
|
||||
let normalized_form = normalize_messaging_platform_form(&platform, raw_form_obj);
|
||||
let form_obj = &normalized_form;
|
||||
|
||||
// 用于后续创建 bindings 的平台信息
|
||||
let saved_account_id = account_id.clone();
|
||||
@@ -655,6 +883,13 @@ pub async fn save_messaging_platform(
|
||||
entry.insert("token".into(), Value::String(t.trim().into()));
|
||||
}
|
||||
entry.insert("enabled".into(), Value::Bool(true));
|
||||
put_string(&mut entry, "dmPolicy", form_string(form_obj, "dmPolicy"));
|
||||
put_string(
|
||||
&mut entry,
|
||||
"groupPolicy",
|
||||
form_string(form_obj, "groupPolicy"),
|
||||
);
|
||||
put_array_from_form_value(&mut entry, "allowFrom", form_obj.get("allowFrom"));
|
||||
|
||||
// guildId + channelId 展开为 guilds 嵌套结构
|
||||
let guild_id = form_obj
|
||||
@@ -711,19 +946,13 @@ pub async fn save_messaging_platform(
|
||||
entry.insert("botToken".into(), Value::String(t.trim().into()));
|
||||
}
|
||||
entry.insert("enabled".into(), Value::Bool(true));
|
||||
|
||||
// allowedUsers 逗号字符串 → allowFrom 数组
|
||||
if let Some(users_str) = form_obj.get("allowedUsers").and_then(|v| v.as_str()) {
|
||||
let users: Vec<Value> = users_str
|
||||
.split(',')
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| Value::String(s.into()))
|
||||
.collect();
|
||||
if !users.is_empty() {
|
||||
entry.insert("allowFrom".into(), Value::Array(users));
|
||||
}
|
||||
}
|
||||
put_string(&mut entry, "dmPolicy", form_string(form_obj, "dmPolicy"));
|
||||
put_string(
|
||||
&mut entry,
|
||||
"groupPolicy",
|
||||
form_string(form_obj, "groupPolicy"),
|
||||
);
|
||||
put_array_from_form_value(&mut entry, "allowFrom", form_obj.get("allowFrom"));
|
||||
|
||||
merge_channel_entry(channels_map, "telegram", entry);
|
||||
}
|
||||
@@ -805,17 +1034,40 @@ pub async fn save_messaging_platform(
|
||||
entry.insert("appId".into(), Value::String(app_id));
|
||||
entry.insert("appSecret".into(), Value::String(app_secret));
|
||||
entry.insert("enabled".into(), Value::Bool(true));
|
||||
entry.insert("connectionMode".into(), Value::String("websocket".into()));
|
||||
|
||||
let domain = form_obj
|
||||
.get("domain")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_string();
|
||||
if !domain.is_empty() {
|
||||
entry.insert("domain".into(), Value::String(domain));
|
||||
}
|
||||
put_string(
|
||||
&mut entry,
|
||||
"connectionMode",
|
||||
form_string(form_obj, "connectionMode"),
|
||||
);
|
||||
put_string(&mut entry, "domain", form_string(form_obj, "domain"));
|
||||
put_string(
|
||||
&mut entry,
|
||||
"webhookPath",
|
||||
form_string(form_obj, "webhookPath"),
|
||||
);
|
||||
put_string(&mut entry, "dmPolicy", form_string(form_obj, "dmPolicy"));
|
||||
put_string(
|
||||
&mut entry,
|
||||
"groupPolicy",
|
||||
form_string(form_obj, "groupPolicy"),
|
||||
);
|
||||
put_string(
|
||||
&mut entry,
|
||||
"reactionNotifications",
|
||||
form_string(form_obj, "reactionNotifications"),
|
||||
);
|
||||
put_array_from_form_value(&mut entry, "allowFrom", form_obj.get("allowFrom"));
|
||||
put_bool_value_if_present(
|
||||
&mut entry,
|
||||
"typingIndicator",
|
||||
form_obj.get("typingIndicator"),
|
||||
);
|
||||
put_bool_value_if_present(
|
||||
&mut entry,
|
||||
"resolveSenderNames",
|
||||
form_obj.get("resolveSenderNames"),
|
||||
);
|
||||
put_bool_value_if_present(&mut entry, "requireMention", form_obj.get("requireMention"));
|
||||
|
||||
// 多账号模式:写入 channels.<storage_key>.accounts.<account_id>
|
||||
if let Some(ref acct) = account_id {
|
||||
@@ -926,13 +1178,19 @@ pub async fn save_messaging_platform(
|
||||
);
|
||||
put_string(&mut entry, "teamId", form_string(form_obj, "teamId"));
|
||||
put_string(&mut entry, "appId", form_string(form_obj, "appId"));
|
||||
put_bool_value_if_present(
|
||||
&mut entry,
|
||||
"userTokenReadOnly",
|
||||
form_obj.get("userTokenReadOnly"),
|
||||
);
|
||||
put_bool_value_if_present(&mut entry, "requireMention", form_obj.get("requireMention"));
|
||||
put_string(&mut entry, "dmPolicy", form_string(form_obj, "dmPolicy"));
|
||||
put_string(
|
||||
&mut entry,
|
||||
"groupPolicy",
|
||||
form_string(form_obj, "groupPolicy"),
|
||||
);
|
||||
put_csv_array_from_form(&mut entry, "allowFrom", &form_string(form_obj, "allowFrom"));
|
||||
put_array_from_form_value(&mut entry, "allowFrom", form_obj.get("allowFrom"));
|
||||
merge_channel_entry(channels_map, &storage_key, entry);
|
||||
}
|
||||
"whatsapp" => {
|
||||
@@ -944,7 +1202,7 @@ pub async fn save_messaging_platform(
|
||||
"groupPolicy",
|
||||
form_string(form_obj, "groupPolicy"),
|
||||
);
|
||||
put_csv_array_from_form(&mut entry, "allowFrom", &form_string(form_obj, "allowFrom"));
|
||||
put_array_from_form_value(&mut entry, "allowFrom", form_obj.get("allowFrom"));
|
||||
put_bool_from_form(&mut entry, "enabled", &form_string(form_obj, "enabled"));
|
||||
merge_channel_entry(channels_map, &storage_key, entry);
|
||||
}
|
||||
@@ -967,7 +1225,7 @@ pub async fn save_messaging_platform(
|
||||
"groupPolicy",
|
||||
form_string(form_obj, "groupPolicy"),
|
||||
);
|
||||
put_csv_array_from_form(&mut entry, "allowFrom", &form_string(form_obj, "allowFrom"));
|
||||
put_array_from_form_value(&mut entry, "allowFrom", form_obj.get("allowFrom"));
|
||||
merge_channel_entry(channels_map, &storage_key, entry);
|
||||
}
|
||||
"matrix" => {
|
||||
@@ -997,7 +1255,7 @@ pub async fn save_messaging_platform(
|
||||
form_string(form_obj, "groupPolicy"),
|
||||
);
|
||||
put_bool_from_form(&mut entry, "e2ee", &form_string(form_obj, "e2ee"));
|
||||
put_csv_array_from_form(&mut entry, "allowFrom", &form_string(form_obj, "allowFrom"));
|
||||
put_array_from_form_value(&mut entry, "allowFrom", form_obj.get("allowFrom"));
|
||||
merge_channel_entry(channels_map, &storage_key, entry);
|
||||
ensure_plugin_allowed(&mut cfg, "matrix")?;
|
||||
}
|
||||
@@ -1029,7 +1287,8 @@ pub async fn save_messaging_platform(
|
||||
"groupPolicy",
|
||||
form_string(form_obj, "groupPolicy"),
|
||||
);
|
||||
put_csv_array_from_form(&mut entry, "allowFrom", &form_string(form_obj, "allowFrom"));
|
||||
put_bool_value_if_present(&mut entry, "requireMention", form_obj.get("requireMention"));
|
||||
put_array_from_form_value(&mut entry, "allowFrom", form_obj.get("allowFrom"));
|
||||
merge_channel_entry(channels_map, &storage_key, entry);
|
||||
ensure_plugin_allowed(&mut cfg, "msteams")?;
|
||||
}
|
||||
@@ -3860,3 +4119,159 @@ async fn verify_dingtalk(
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn normalize_channel_form_adds_telegram_access_defaults() {
|
||||
let form = json!({
|
||||
"botToken": "123:token"
|
||||
});
|
||||
let normalized =
|
||||
normalize_messaging_platform_form("telegram", form.as_object().expect("object"));
|
||||
|
||||
assert_eq!(
|
||||
normalized.get("botToken").and_then(|v| v.as_str()),
|
||||
Some("123:token")
|
||||
);
|
||||
assert_eq!(
|
||||
normalized.get("dmPolicy").and_then(|v| v.as_str()),
|
||||
Some("pairing")
|
||||
);
|
||||
assert_eq!(
|
||||
normalized.get("groupPolicy").and_then(|v| v.as_str()),
|
||||
Some("allowlist")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_channel_form_converts_legacy_ui_policy_values() {
|
||||
let form = json!({
|
||||
"mode": "socket",
|
||||
"botToken": "xoxb-token",
|
||||
"appToken": "xapp-token",
|
||||
"dmPolicy": "allow",
|
||||
"groupPolicy": "mentioned"
|
||||
});
|
||||
let normalized =
|
||||
normalize_messaging_platform_form("slack", form.as_object().expect("object"));
|
||||
|
||||
assert_eq!(
|
||||
normalized.get("dmPolicy").and_then(|v| v.as_str()),
|
||||
Some("open")
|
||||
);
|
||||
assert_eq!(
|
||||
normalized
|
||||
.get("allowFrom")
|
||||
.and_then(|v| v.as_array())
|
||||
.cloned(),
|
||||
Some(vec![Value::String("*".into())])
|
||||
);
|
||||
assert_eq!(
|
||||
normalized.get("groupPolicy").and_then(|v| v.as_str()),
|
||||
Some("open")
|
||||
);
|
||||
assert_eq!(
|
||||
normalized.get("requireMention").and_then(|v| v.as_bool()),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
normalized.get("webhookPath").and_then(|v| v.as_str()),
|
||||
Some("/slack/events")
|
||||
);
|
||||
assert_eq!(
|
||||
normalized
|
||||
.get("userTokenReadOnly")
|
||||
.and_then(|v| v.as_bool()),
|
||||
Some(false)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_channel_form_avoids_unsupported_top_level_require_mention() {
|
||||
let form = json!({
|
||||
"account": "+15551234567",
|
||||
"dmPolicy": "deny",
|
||||
"groupPolicy": "mentioned"
|
||||
});
|
||||
let normalized =
|
||||
normalize_messaging_platform_form("signal", form.as_object().expect("object"));
|
||||
|
||||
assert_eq!(
|
||||
normalized.get("dmPolicy").and_then(|v| v.as_str()),
|
||||
Some("disabled")
|
||||
);
|
||||
assert_eq!(
|
||||
normalized.get("groupPolicy").and_then(|v| v.as_str()),
|
||||
Some("open")
|
||||
);
|
||||
assert!(!normalized.contains_key("requireMention"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_channel_form_adds_feishu_required_defaults() {
|
||||
let form = json!({
|
||||
"appId": "cli_a",
|
||||
"appSecret": "secret",
|
||||
"domain": ""
|
||||
});
|
||||
let normalized =
|
||||
normalize_messaging_platform_form("feishu", form.as_object().expect("object"));
|
||||
|
||||
assert_eq!(
|
||||
normalized.get("domain").and_then(|v| v.as_str()),
|
||||
Some("feishu")
|
||||
);
|
||||
assert_eq!(
|
||||
normalized.get("connectionMode").and_then(|v| v.as_str()),
|
||||
Some("websocket")
|
||||
);
|
||||
assert_eq!(
|
||||
normalized.get("webhookPath").and_then(|v| v.as_str()),
|
||||
Some("/feishu/events")
|
||||
);
|
||||
assert_eq!(
|
||||
normalized.get("dmPolicy").and_then(|v| v.as_str()),
|
||||
Some("pairing")
|
||||
);
|
||||
assert_eq!(
|
||||
normalized.get("groupPolicy").and_then(|v| v.as_str()),
|
||||
Some("allowlist")
|
||||
);
|
||||
assert_eq!(
|
||||
normalized
|
||||
.get("reactionNotifications")
|
||||
.and_then(|v| v.as_str()),
|
||||
Some("off")
|
||||
);
|
||||
assert_eq!(
|
||||
normalized.get("typingIndicator").and_then(|v| v.as_bool()),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
normalized
|
||||
.get("resolveSenderNames")
|
||||
.and_then(|v| v.as_bool()),
|
||||
Some(true)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_form_readback_preserves_mention_policy_choice() {
|
||||
let saved = json!({
|
||||
"groupPolicy": "open",
|
||||
"requireMention": true,
|
||||
"allowFrom": ["U123"]
|
||||
});
|
||||
let mut form = Map::new();
|
||||
insert_access_policy_form_values(&mut form, &saved, false, true);
|
||||
|
||||
assert_eq!(
|
||||
form.get("groupPolicy").and_then(|v| v.as_str()),
|
||||
Some("mentioned")
|
||||
);
|
||||
assert_eq!(form.get("allowFrom").and_then(|v| v.as_str()), Some("U123"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,10 +85,15 @@ export default {
|
||||
policyDefault: _('默认', 'Default', '預設'),
|
||||
dmAllow: _('允许私信', 'Allow DMs', '允許私信'),
|
||||
dmDeny: _('拒绝私信', 'Deny DMs', '拒絕私信'),
|
||||
dmPairing: _('配对 / 白名单', 'Pairing / allowlist', '配對 / 白名單'),
|
||||
dmOpen: _('允许所有私信', 'Allow all DMs', '允許所有私信'),
|
||||
dmAllowlist: _('仅白名单私信', 'Allowlist only', '僅白名單私信'),
|
||||
dmDisabled: _('禁用私信', 'Disable DMs', '停用私信'),
|
||||
groupPolicy: _('群组策略', 'Group Policy', '群組策略'),
|
||||
groupAllChannels: _('所有频道', 'All channels', '所有頻道'),
|
||||
groupMentionOnly: _('仅 @提及时', 'Only when @mentioned', '僅 @提及時'),
|
||||
groupAllowlist: _('白名单', 'Allowlist', '白名單'),
|
||||
groupDisabled: _('禁用群组', 'Disable groups', '停用群組'),
|
||||
allowFromPh: _('可选,逗号分隔用户/频道 ID', 'Optional, comma-separated user/channel IDs', '可選,逗號分隔使用者/頻道 ID'),
|
||||
allowFromHint: _('限制允许的用户或频道 ID,留空不限制', 'Restrict to specific user or channel IDs; leave empty for no restriction', '限制允許的使用者或頻道 ID,留空不限制'),
|
||||
weixinLabel: _('微信', 'WeChat'),
|
||||
|
||||
@@ -20,6 +20,22 @@ import {
|
||||
|
||||
// ── 渠道注册表:面板内置向导,覆盖 OpenClaw 官方渠道 + 国内扩展渠道 ──
|
||||
|
||||
const DM_POLICY_OPTIONS = [
|
||||
{ value: '', label: t('channels.policyDefault') },
|
||||
{ value: 'pairing', label: t('channels.dmPairing') },
|
||||
{ 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 },
|
||||
...(mention ? [{ value: 'mentioned', label: t('channels.groupMentionOnly') }] : []),
|
||||
{ value: 'allowlist', label: t('channels.groupAllowlist') },
|
||||
{ value: 'disabled', label: t('channels.groupDisabled') },
|
||||
]
|
||||
|
||||
const PLATFORM_REGISTRY = {
|
||||
qqbot: {
|
||||
label: t('channels.qqbotLabel'),
|
||||
@@ -81,11 +97,14 @@ const PLATFORM_REGISTRY = {
|
||||
{
|
||||
key: 'domain', label: t('channels.feishuDomainLabel'), type: 'select',
|
||||
options: [
|
||||
{ value: '', label: t('channels.feishuDomainFeishu') },
|
||||
{ value: 'feishu', label: t('channels.feishuDomainFeishu') },
|
||||
{ value: 'lark', label: t('channels.feishuDomainLark') },
|
||||
],
|
||||
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.groupAllGroups'), { mention: true }), required: false },
|
||||
{ key: 'allowFrom', label: 'Allow From', placeholder: t('channels.allowFromPh'), required: false, hint: t('channels.allowFromHint') },
|
||||
],
|
||||
pluginRequired: '@larksuite/openclaw-lark@latest',
|
||||
pluginId: 'openclaw-lark',
|
||||
@@ -104,6 +123,9 @@ const PLATFORM_REGISTRY = {
|
||||
guideFooter: t('channels.telegramGuideFooter'),
|
||||
fields: [
|
||||
{ key: 'botToken', label: 'Bot Token', placeholder: '123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11', secret: true, required: true },
|
||||
{ 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.groupAllGroups')), required: false },
|
||||
{ key: 'allowFrom', label: 'Allow From', placeholder: t('channels.allowFromPh'), required: false, hint: t('channels.allowFromHint') },
|
||||
],
|
||||
configKey: 'telegram',
|
||||
pairingChannel: 'telegram',
|
||||
@@ -121,6 +143,9 @@ const PLATFORM_REGISTRY = {
|
||||
guideFooter: t('channels.discordGuideFooter'),
|
||||
fields: [
|
||||
{ key: 'token', label: 'Bot Token', placeholder: 'MTExxxxxxxxx.Gxxxxxx.xxxxxxxx', secret: true, required: true },
|
||||
{ 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.groupAllChannels')), required: false },
|
||||
{ key: 'allowFrom', label: 'Allow From', placeholder: t('channels.allowFromPh'), required: false, hint: t('channels.allowFromHint') },
|
||||
],
|
||||
configKey: 'discord',
|
||||
pairingChannel: 'discord',
|
||||
@@ -150,8 +175,8 @@ const PLATFORM_REGISTRY = {
|
||||
{ key: 'signingSecret', label: 'Signing Secret', placeholder: t('channels.slackSigningSecretPh'), secret: true, requiredWhen: { mode: 'http' }, hint: t('channels.slackSigningSecretHint') },
|
||||
{ key: 'teamId', label: 'Team ID', placeholder: t('channels.slackTeamIdPh'), required: false },
|
||||
{ key: 'webhookPath', label: 'Webhook Path', placeholder: t('channels.slackWebhookPathPh'), required: false },
|
||||
{ key: 'dmPolicy', label: t('channels.dmPolicy'), type: 'select', options: [{ value: '', label: t('channels.policyDefault') }, { value: 'allow', label: t('channels.dmAllow') }, { value: 'deny', label: t('channels.dmDeny') }], required: false },
|
||||
{ key: 'groupPolicy', label: t('channels.groupPolicy'), type: 'select', options: [{ value: '', label: t('channels.policyDefault') }, { value: 'all', label: t('channels.groupAllChannels') }, { value: 'mentioned', label: t('channels.groupMentionOnly') }, { value: 'allowlist', label: t('channels.groupAllowlist') }], 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.groupAllChannels'), { mention: true }), required: false },
|
||||
{ key: 'allowFrom', label: 'Allow From', placeholder: t('channels.allowFromPh'), required: false, hint: t('channels.allowFromHint') },
|
||||
],
|
||||
configKey: 'slack',
|
||||
@@ -196,8 +221,8 @@ const PLATFORM_REGISTRY = {
|
||||
{ key: 'tenantId', label: 'Tenant ID', placeholder: t('channels.msteamsTenantIdPh'), required: false },
|
||||
{ key: 'botEndpoint', label: 'Bot Endpoint', placeholder: 'https://example.com/api/teams/messages', required: false },
|
||||
{ key: 'webhookPath', label: 'Webhook Path', placeholder: '/msteams/messages', required: false },
|
||||
{ key: 'dmPolicy', label: t('channels.dmPolicy'), type: 'select', options: [{ value: '', label: t('channels.policyDefault') }, { value: 'allow', label: t('channels.dmAllow') }, { value: 'deny', label: t('channels.dmDeny') }], required: false },
|
||||
{ key: 'groupPolicy', label: t('channels.groupPolicy'), type: 'select', options: [{ value: '', label: t('channels.policyDefault') }, { value: 'all', label: t('channels.groupAllTeams') }, { value: 'mentioned', label: t('channels.groupMentionOnly') }, { value: 'allowlist', label: t('channels.groupAllowlist') }], 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.groupAllTeams'), { mention: true }), required: false },
|
||||
{ key: 'allowFrom', label: 'Allow From', placeholder: t('channels.msteamsAllowFromPh'), required: false },
|
||||
],
|
||||
configKey: 'msteams',
|
||||
@@ -220,8 +245,8 @@ const PLATFORM_REGISTRY = {
|
||||
{ key: 'httpUrl', label: 'HTTP URL', placeholder: t('channels.optionalEg', { example: 'http://127.0.0.1:8080' }), required: false },
|
||||
{ key: 'httpHost', label: 'HTTP Host', placeholder: t('channels.optionalEg', { example: '127.0.0.1' }), required: false },
|
||||
{ key: 'httpPort', label: 'HTTP Port', placeholder: t('channels.optionalEg', { example: '8080' }), required: false },
|
||||
{ key: 'dmPolicy', label: t('channels.dmPolicy'), type: 'select', options: [{ value: '', label: t('channels.policyDefault') }, { value: 'allow', label: t('channels.dmAllow') }, { value: 'deny', label: t('channels.dmDeny') }], required: false },
|
||||
{ key: 'groupPolicy', label: t('channels.groupPolicy'), type: 'select', options: [{ value: '', label: t('channels.policyDefault') }, { value: 'all', label: t('channels.groupAllGroups') }, { value: 'mentioned', label: t('channels.groupMentionBot') }, { value: 'allowlist', label: t('channels.groupAllowlist') }], 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.groupAllGroups')), required: false },
|
||||
{ key: 'allowFrom', label: 'Allow From', placeholder: t('channels.signalAllowFromPh'), required: false },
|
||||
],
|
||||
configKey: 'signal',
|
||||
@@ -243,8 +268,8 @@ const PLATFORM_REGISTRY = {
|
||||
{ key: 'password', label: 'Password', placeholder: t('channels.matrixPasswordPh'), secret: true, required: false },
|
||||
{ key: 'deviceId', label: 'Device ID', placeholder: t('channels.optionalEg', { example: 'CLAWPANEL' }), required: false },
|
||||
{ key: 'e2ee', label: 'E2EE', type: 'select', options: [{ value: '', label: t('channels.policyDefault') }, { value: 'true', label: t('channels.enable') }, { value: 'false', label: t('channels.disable') }], required: false },
|
||||
{ key: 'dmPolicy', label: t('channels.dmPolicy'), type: 'select', options: [{ value: '', label: t('channels.policyDefault') }, { value: 'allow', label: t('channels.dmAllow') }, { value: 'deny', label: t('channels.dmDeny') }], required: false },
|
||||
{ key: 'groupPolicy', label: t('channels.groupPolicy'), type: 'select', options: [{ value: '', label: t('channels.policyDefault') }, { value: 'all', label: t('channels.groupAllRooms') }, { value: 'mentioned', label: t('channels.groupMentionBot') }, { value: 'allowlist', label: t('channels.groupAllowlist') }], 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.groupAllRooms')), required: false },
|
||||
{ key: 'allowFrom', label: 'Allow From', placeholder: t('channels.matrixAllowFromPh'), required: false },
|
||||
],
|
||||
configKey: 'matrix',
|
||||
|
||||
161
tests/channel-config-normalization.test.js
Normal file
161
tests/channel-config-normalization.test.js
Normal file
@@ -0,0 +1,161 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
buildMessagingPlatformFormValues,
|
||||
normalizeMessagingPlatformForm,
|
||||
} from '../scripts/dev-api.js'
|
||||
|
||||
test('渠道保存会为 Telegram 补齐新版 OpenClaw 必填访问策略', () => {
|
||||
const form = normalizeMessagingPlatformForm('telegram', {
|
||||
botToken: '123:token',
|
||||
})
|
||||
|
||||
assert.equal(form.botToken, '123:token')
|
||||
assert.equal(form.dmPolicy, 'pairing')
|
||||
assert.equal(form.groupPolicy, 'allowlist')
|
||||
})
|
||||
|
||||
test('渠道保存会把旧 UI 策略值转换为 OpenClaw 支持的枚举', () => {
|
||||
const form = normalizeMessagingPlatformForm('slack', {
|
||||
mode: 'socket',
|
||||
botToken: 'xoxb-token',
|
||||
appToken: 'xapp-token',
|
||||
dmPolicy: 'allow',
|
||||
groupPolicy: 'mentioned',
|
||||
})
|
||||
|
||||
assert.equal(form.dmPolicy, 'open')
|
||||
assert.deepEqual(form.allowFrom, ['*'])
|
||||
assert.equal(form.groupPolicy, 'open')
|
||||
assert.equal(form.requireMention, true)
|
||||
assert.equal(form.webhookPath, '/slack/events')
|
||||
assert.equal(form.userTokenReadOnly, false)
|
||||
})
|
||||
|
||||
test('渠道保存不会向不支持顶层 requireMention 的平台写入非法字段', () => {
|
||||
const form = normalizeMessagingPlatformForm('signal', {
|
||||
account: '+15551234567',
|
||||
dmPolicy: 'deny',
|
||||
groupPolicy: 'mentioned',
|
||||
})
|
||||
|
||||
assert.equal(form.dmPolicy, 'disabled')
|
||||
assert.equal(form.groupPolicy, 'open')
|
||||
assert.equal(Object.hasOwn(form, 'requireMention'), false)
|
||||
})
|
||||
|
||||
test('渠道保存会为飞书补齐新版内核要求的默认字段', () => {
|
||||
const form = normalizeMessagingPlatformForm('feishu', {
|
||||
appId: 'cli_a',
|
||||
appSecret: 'secret',
|
||||
domain: '',
|
||||
})
|
||||
|
||||
assert.equal(form.domain, 'feishu')
|
||||
assert.equal(form.connectionMode, 'websocket')
|
||||
assert.equal(form.webhookPath, '/feishu/events')
|
||||
assert.equal(form.dmPolicy, 'pairing')
|
||||
assert.equal(form.groupPolicy, 'allowlist')
|
||||
assert.equal(form.reactionNotifications, 'off')
|
||||
assert.equal(form.typingIndicator, true)
|
||||
assert.equal(form.resolveSenderNames, true)
|
||||
})
|
||||
|
||||
test('渠道读取会把新版访问策略字段回显为表单可编辑值', () => {
|
||||
const values = buildMessagingPlatformFormValues('telegram', {
|
||||
botToken: '123:token',
|
||||
dmPolicy: 'allowlist',
|
||||
groupPolicy: 'disabled',
|
||||
allowFrom: ['u-1', 'u-2'],
|
||||
})
|
||||
|
||||
assert.equal(values.botToken, '123:token')
|
||||
assert.equal(values.dmPolicy, 'allowlist')
|
||||
assert.equal(values.groupPolicy, 'disabled')
|
||||
assert.equal(values.allowFrom, 'u-1, u-2')
|
||||
assert.equal(values.allowedUsers, 'u-1, u-2')
|
||||
})
|
||||
|
||||
test('渠道读取会合并飞书账号凭证和根节点共享策略字段', () => {
|
||||
const values = buildMessagingPlatformFormValues(
|
||||
'feishu',
|
||||
{
|
||||
appId: 'cli_a',
|
||||
appSecret: 'secret',
|
||||
},
|
||||
{
|
||||
channelRoot: {
|
||||
domain: 'lark',
|
||||
connectionMode: 'websocket',
|
||||
webhookPath: '/feishu/events',
|
||||
dmPolicy: 'pairing',
|
||||
groupPolicy: 'allowlist',
|
||||
reactionNotifications: 'off',
|
||||
typingIndicator: true,
|
||||
resolveSenderNames: false,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert.equal(values.appId, 'cli_a')
|
||||
assert.equal(values.appSecret, 'secret')
|
||||
assert.equal(values.domain, 'lark')
|
||||
assert.equal(values.connectionMode, 'websocket')
|
||||
assert.equal(values.webhookPath, '/feishu/events')
|
||||
assert.equal(values.dmPolicy, 'pairing')
|
||||
assert.equal(values.groupPolicy, 'allowlist')
|
||||
assert.equal(values.reactionNotifications, 'off')
|
||||
assert.equal(values.typingIndicator, 'true')
|
||||
assert.equal(values.resolveSenderNames, 'false')
|
||||
})
|
||||
|
||||
test('渠道读取飞书多账号时不会用根节点旧凭证覆盖账号凭证', () => {
|
||||
const values = buildMessagingPlatformFormValues(
|
||||
'feishu',
|
||||
{
|
||||
appId: 'account_app',
|
||||
appSecret: 'account_secret',
|
||||
dmPolicy: 'pairing',
|
||||
},
|
||||
{
|
||||
channelRoot: {
|
||||
appId: 'root_app',
|
||||
appSecret: 'root_secret',
|
||||
domain: 'lark',
|
||||
groupPolicy: 'allowlist',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert.equal(values.appId, 'account_app')
|
||||
assert.equal(values.appSecret, 'account_secret')
|
||||
assert.equal(values.domain, 'lark')
|
||||
assert.equal(values.dmPolicy, 'pairing')
|
||||
assert.equal(values.groupPolicy, 'allowlist')
|
||||
})
|
||||
|
||||
test('渠道读取会把 open + requireMention 反向回显为仅提及时策略', () => {
|
||||
const values = buildMessagingPlatformFormValues('slack', {
|
||||
mode: 'socket',
|
||||
botToken: 'xoxb-token',
|
||||
appToken: 'xapp-token',
|
||||
groupPolicy: 'open',
|
||||
requireMention: true,
|
||||
})
|
||||
|
||||
assert.equal(values.groupPolicy, 'mentioned')
|
||||
assert.equal(values.requireMention, 'true')
|
||||
})
|
||||
|
||||
test('渠道保存会在用户改回所有群组时显式清除仅提及开关', () => {
|
||||
const form = normalizeMessagingPlatformForm('slack', {
|
||||
mode: 'socket',
|
||||
botToken: 'xoxb-token',
|
||||
appToken: 'xapp-token',
|
||||
groupPolicy: 'open',
|
||||
})
|
||||
|
||||
assert.equal(form.groupPolicy, 'open')
|
||||
assert.equal(form.requireMention, false)
|
||||
})
|
||||
Reference in New Issue
Block a user