mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 04:10:00 +08:00
feat(channels): add Tlon config compatibility
This commit is contained in:
@@ -2498,7 +2498,7 @@ export function normalizeMessagingPlatformForm(platform, form = {}) {
|
||||
normalized.allowedUserIds = csvToStringArray(normalized.allowedUserIds)
|
||||
}
|
||||
|
||||
for (const key of ['promptStarters', 'delegatedAuthScopes', 'attachmentRoots', 'remoteAttachmentRoots', 'toolsAllow', 'allowedRoles', 'relays', 'channels', 'groups', 'mentionPatterns']) {
|
||||
for (const key of ['promptStarters', 'delegatedAuthScopes', 'attachmentRoots', 'remoteAttachmentRoots', 'toolsAllow', 'allowedRoles', 'relays', 'channels', 'groups', 'mentionPatterns', 'groupChannels', 'dmAllowlist', 'groupInviteAllowlist', 'defaultAuthorizedShips']) {
|
||||
if (Object.hasOwn(normalized, key)) normalized[key] = csvToStringArray(normalized[key])
|
||||
}
|
||||
|
||||
@@ -2515,7 +2515,7 @@ export function normalizeMessagingPlatformForm(platform, form = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of ['dangerouslyAllowNameMatching', 'dangerouslyAllowPrivateNetwork', 'dangerouslyAllowInheritedWebhookPath', 'allowInsecureSsl', 'enabled', 'allowBots', 'blockStreaming', 'useManagedIdentity', 'typingIndicator', 'welcomeCard', 'groupWelcomeCard', 'feedbackEnabled', 'feedbackReflection', 'delegatedAuthEnabled', 'ssoEnabled', 'configWrites', 'includeAttachments', 'sendReadReceipts', 'coalesceSameSenderDms', 'selfChatMode', 'ackDirect', 'senderIsOwner', 'requireMention', 'tls', 'nickservEnabled', 'nickservRegister']) {
|
||||
for (const key of ['dangerouslyAllowNameMatching', 'dangerouslyAllowPrivateNetwork', 'dangerouslyAllowInheritedWebhookPath', 'allowInsecureSsl', 'enabled', 'allowBots', 'blockStreaming', 'useManagedIdentity', 'typingIndicator', 'welcomeCard', 'groupWelcomeCard', 'feedbackEnabled', 'feedbackReflection', 'delegatedAuthEnabled', 'ssoEnabled', 'configWrites', 'includeAttachments', 'sendReadReceipts', 'coalesceSameSenderDms', 'selfChatMode', 'ackDirect', 'senderIsOwner', 'requireMention', 'tls', 'nickservEnabled', 'nickservRegister', 'autoDiscoverChannels', 'showModelSignature', 'autoAcceptDmInvites', 'autoAcceptGroupInvites']) {
|
||||
if (Object.hasOwn(normalized, key)) {
|
||||
const value = typeof normalized[key] === 'boolean'
|
||||
? String(normalized[key])
|
||||
@@ -2645,6 +2645,7 @@ const MESSAGING_CREDENTIAL_FIELDS = [
|
||||
'botToken',
|
||||
'channelAccessToken',
|
||||
'channelSecret',
|
||||
'code',
|
||||
'clientId',
|
||||
'clientSecret',
|
||||
'refreshToken',
|
||||
@@ -2744,6 +2745,7 @@ const CHANNEL_DIAG_REQUIRED_FIELDS = {
|
||||
'nextcloud-talk': [['baseUrl', 'Base URL']],
|
||||
nostr: [['privateKey', 'Private Key']],
|
||||
irc: [['host', 'Host'], ['nick', 'Nick']],
|
||||
tlon: [['ship', 'Ship'], ['url', 'URL'], ['code', 'Code']],
|
||||
twitch: [['username', 'Username'], ['accessToken', 'Access Token'], ['clientId', 'Client ID'], ['channel', 'Channel']],
|
||||
signal: [['account', 'Signal 账号']],
|
||||
}
|
||||
@@ -3211,6 +3213,26 @@ export function buildMessagingPlatformFormValues(platform, saved = {}, options =
|
||||
return form
|
||||
}
|
||||
|
||||
if (storageKey === 'tlon') {
|
||||
const shared = options.channelRoot && typeof options.channelRoot === 'object'
|
||||
? { ...options.channelRoot, ...saved }
|
||||
: saved
|
||||
if (options.channelRoot?.network && !saved.network) shared.network = options.channelRoot.network
|
||||
for (const key of ['name', 'ship', 'url', 'code', 'responsePrefix', 'ownerShip']) {
|
||||
putSecretAwareFormValue(form, shared, key)
|
||||
}
|
||||
putBoolFormValue(form, shared, 'enabled')
|
||||
putBoolFormValue(form, shared?.network, 'dangerouslyAllowPrivateNetwork')
|
||||
putCsvFormValue(form, shared, 'groupChannels')
|
||||
putCsvFormValue(form, shared, 'dmAllowlist')
|
||||
putCsvFormValue(form, shared, 'groupInviteAllowlist')
|
||||
putCsvFormValue(form, shared, 'defaultAuthorizedShips')
|
||||
for (const key of ['autoDiscoverChannels', 'showModelSignature', 'autoAcceptDmInvites', 'autoAcceptGroupInvites']) {
|
||||
putBoolFormValue(form, shared, key)
|
||||
}
|
||||
return form
|
||||
}
|
||||
|
||||
if (storageKey === 'synology-chat') {
|
||||
for (const key of ['token', 'incomingUrl', 'nasHost', 'webhookPath', 'botName']) {
|
||||
putSecretAwareFormValue(form, saved, key)
|
||||
@@ -3660,6 +3682,7 @@ function secretAwareAccountDisplayValue(value) {
|
||||
function resolvePlatformConfigEntry(channelRoot, platform, accountId) {
|
||||
if (!channelRoot || typeof channelRoot !== 'object') return null
|
||||
const accountKey = typeof accountId === 'string' ? accountId.trim() : ''
|
||||
if (platformStorageKey(platform) === 'tlon' && accountKey === QQBOT_DEFAULT_ACCOUNT_ID) return channelRoot
|
||||
if (accountKey) return channelRoot.accounts?.[accountKey] || channelRoot
|
||||
if (platformStorageKey(platform) === 'qqbot' && !channelHasQqbotCredentials(channelRoot)) {
|
||||
return channelRoot.accounts?.[QQBOT_DEFAULT_ACCOUNT_ID] || channelRoot
|
||||
@@ -3674,7 +3697,7 @@ export function listPlatformAccounts(channelRoot) {
|
||||
return Object.entries(channelRoot.accounts)
|
||||
.map(([accountId, value]) => {
|
||||
const entry = { accountId }
|
||||
const displayId = ['appId', 'clientId', 'account', 'nick']
|
||||
const displayId = ['appId', 'clientId', 'account', 'nick', 'ship']
|
||||
.map(key => secretAwareAccountDisplayValue(value?.[key]))
|
||||
.find(Boolean)
|
||||
if (displayId) entry.appId = displayId
|
||||
@@ -4056,6 +4079,24 @@ function buildOpenClawMessagingPlatformEntry(platform, form, currentSaved = {})
|
||||
if (typeof form.nickservRegister === 'boolean') nickserv.register = form.nickservRegister
|
||||
if (form.nickservRegisterEmail) nickserv.registerEmail = form.nickservRegisterEmail
|
||||
if (Object.keys(nickserv).length) entry.nickserv = nickserv
|
||||
} else if (storageKey === 'tlon') {
|
||||
entry.enabled = typeof form.enabled === 'boolean' ? form.enabled : true
|
||||
for (const key of ['name', 'ship', 'url', 'responsePrefix', 'ownerShip']) {
|
||||
if (form[key]) entry[key] = form[key]
|
||||
}
|
||||
const code = resolveMessagingCredentialFormValueForSave({ form, current: currentSaved, formKey: 'code' })
|
||||
if (code === undefined) delete entry.code
|
||||
else entry.code = code
|
||||
if (Array.isArray(form.groupChannels) && form.groupChannels.length) entry.groupChannels = form.groupChannels
|
||||
if (Array.isArray(form.dmAllowlist) && form.dmAllowlist.length) entry.dmAllowlist = form.dmAllowlist
|
||||
if (Array.isArray(form.groupInviteAllowlist) && form.groupInviteAllowlist.length) entry.groupInviteAllowlist = form.groupInviteAllowlist
|
||||
if (Array.isArray(form.defaultAuthorizedShips) && form.defaultAuthorizedShips.length) entry.defaultAuthorizedShips = form.defaultAuthorizedShips
|
||||
for (const key of ['autoDiscoverChannels', 'showModelSignature', 'autoAcceptDmInvites', 'autoAcceptGroupInvites']) {
|
||||
if (typeof form[key] === 'boolean') entry[key] = form[key]
|
||||
}
|
||||
if (typeof form.dangerouslyAllowPrivateNetwork === 'boolean') {
|
||||
entry.network = { ...(currentSaved?.network || {}), dangerouslyAllowPrivateNetwork: form.dangerouslyAllowPrivateNetwork }
|
||||
}
|
||||
} else if (storageKey === 'synology-chat') {
|
||||
for (const key of ['token', 'incomingUrl', 'nasHost', 'webhookPath', 'botName']) {
|
||||
if (form[key]) entry[key] = form[key]
|
||||
@@ -4096,8 +4137,11 @@ export function mergeOpenClawMessagingPlatformConfig(cfg, { platform, form, acco
|
||||
const normalizedAccountId = typeof accountId === 'string' ? accountId.trim() : ''
|
||||
const currentSaved = resolvePlatformConfigEntry(cfg.channels?.[storageKey], platform, normalizedAccountId) || {}
|
||||
const entry = buildOpenClawMessagingPlatformEntry(platform, normalizedForm, currentSaved)
|
||||
applyMessagingPlatformEntry(cfg, storageKey, storageKey === 'nostr' ? '' : normalizedAccountId, entry)
|
||||
if (['zalo', 'zalouser', 'line', 'mattermost', 'clickclack', 'nextcloud-talk', 'twitch', 'nostr', 'irc', 'synology-chat', 'googlechat', 'msteams', 'imessage', 'whatsapp'].includes(storageKey)) {
|
||||
const targetAccountId = storageKey === 'nostr' || (storageKey === 'tlon' && normalizedAccountId === QQBOT_DEFAULT_ACCOUNT_ID)
|
||||
? ''
|
||||
: normalizedAccountId
|
||||
applyMessagingPlatformEntry(cfg, storageKey, targetAccountId, entry)
|
||||
if (['zalo', 'zalouser', 'line', 'mattermost', 'clickclack', 'nextcloud-talk', 'twitch', 'nostr', 'irc', 'tlon', 'synology-chat', 'googlechat', 'msteams', 'imessage', 'whatsapp'].includes(storageKey)) {
|
||||
ensureMessagingPluginAllowed(cfg, storageKey)
|
||||
}
|
||||
return { entry, accountId: normalizedAccountId, storageKey }
|
||||
@@ -5567,16 +5611,19 @@ const handlers = {
|
||||
} else {
|
||||
setRootChannelEntry(entry)
|
||||
}
|
||||
} else if (['line', 'mattermost', 'clickclack', 'nextcloud-talk', 'twitch', 'nostr', 'irc', 'synology-chat', 'googlechat', 'msteams', 'whatsapp'].includes(storageKey)) {
|
||||
} else if (['line', 'mattermost', 'clickclack', 'nextcloud-talk', 'twitch', 'nostr', 'irc', 'tlon', 'synology-chat', 'googlechat', 'msteams', 'whatsapp'].includes(storageKey)) {
|
||||
const built = buildOpenClawMessagingPlatformEntry(platform, form, currentSaved)
|
||||
applyMessagingPlatformEntry(cfg, storageKey, storageKey === 'nostr' ? '' : normalizedAccountId, built)
|
||||
const targetAccountId = storageKey === 'nostr' || (storageKey === 'tlon' && normalizedAccountId === QQBOT_DEFAULT_ACCOUNT_ID)
|
||||
? ''
|
||||
: normalizedAccountId
|
||||
applyMessagingPlatformEntry(cfg, storageKey, targetAccountId, built)
|
||||
ensureMessagingPluginAllowed(cfg, storageKey)
|
||||
} else {
|
||||
Object.assign(entry, form)
|
||||
preserveMessagingCredentialRefs(entry, form, currentSaved)
|
||||
}
|
||||
|
||||
if (platform !== 'qqbot' && platform !== 'feishu' && platform !== 'dingtalk' && platform !== 'dingtalk-connector' && !['line', 'mattermost', 'clickclack', 'nextcloud-talk', 'twitch', 'nostr', 'irc', 'synology-chat', 'googlechat', 'msteams', 'whatsapp'].includes(storageKey)) {
|
||||
if (platform !== 'qqbot' && platform !== 'feishu' && platform !== 'dingtalk' && platform !== 'dingtalk-connector' && !['line', 'mattermost', 'clickclack', 'nextcloud-talk', 'twitch', 'nostr', 'irc', 'tlon', 'synology-chat', 'googlechat', 'msteams', 'whatsapp'].includes(storageKey)) {
|
||||
preserveMessagingCredentialRefs(entry, form, currentSaved)
|
||||
// 合并模式:保留用户通过 CLI 或手动编辑的自定义字段
|
||||
applyMessagingPlatformEntry(cfg, storageKey, normalizedAccountId, entry)
|
||||
@@ -5716,6 +5763,9 @@ const handlers = {
|
||||
if (platform === 'irc') {
|
||||
return { valid: true, warnings: ['IRC 面板已完成基础字段校验;实际连通性请通过 Gateway 启动日志或 openclaw channels status --probe 验证。'] }
|
||||
}
|
||||
if (platform === 'tlon') {
|
||||
return { valid: true, warnings: ['Tlon 面板已完成基础字段校验;实际连通性请通过 Gateway 启动日志或 openclaw channels status --probe 验证。'] }
|
||||
}
|
||||
if (platform === 'discord') {
|
||||
try {
|
||||
const resp = await fetch('https://discord.com/api/v10/users/@me', {
|
||||
|
||||
@@ -197,6 +197,7 @@ fn preserve_messaging_credential_refs(
|
||||
"botToken",
|
||||
"channelAccessToken",
|
||||
"channelSecret",
|
||||
"code",
|
||||
"clientId",
|
||||
"clientSecret",
|
||||
"refreshToken",
|
||||
@@ -286,6 +287,7 @@ fn channel_root_has_messaging_credential(root: &Map<String, Value>) -> bool {
|
||||
"botToken",
|
||||
"channelAccessToken",
|
||||
"channelSecret",
|
||||
"code",
|
||||
"clientId",
|
||||
"clientSecret",
|
||||
"refreshToken",
|
||||
@@ -332,6 +334,7 @@ fn required_channel_credential_fields(
|
||||
"nextcloud-talk" => vec![("baseUrl", "Base URL")],
|
||||
"nostr" => vec![("privateKey", "Private Key")],
|
||||
"irc" => vec![("host", "Host"), ("nick", "Nick")],
|
||||
"tlon" => vec![("ship", "Ship"), ("url", "URL"), ("code", "Code")],
|
||||
"twitch" => vec![
|
||||
("username", "Username"),
|
||||
("accessToken", "Access Token"),
|
||||
@@ -1081,6 +1084,10 @@ fn normalize_messaging_platform_form(
|
||||
"channels",
|
||||
"groups",
|
||||
"mentionPatterns",
|
||||
"groupChannels",
|
||||
"dmAllowlist",
|
||||
"groupInviteAllowlist",
|
||||
"defaultAuthorizedShips",
|
||||
] {
|
||||
if normalized.contains_key(key) {
|
||||
let items = json_array_from_csv_value(normalized.get(key));
|
||||
@@ -1115,6 +1122,10 @@ fn normalize_messaging_platform_form(
|
||||
"tls",
|
||||
"nickservEnabled",
|
||||
"nickservRegister",
|
||||
"autoDiscoverChannels",
|
||||
"showModelSignature",
|
||||
"autoAcceptDmInvites",
|
||||
"autoAcceptGroupInvites",
|
||||
] {
|
||||
if normalized.contains_key(key) {
|
||||
let value = match normalized.get(key) {
|
||||
@@ -1409,6 +1420,9 @@ fn resolve_platform_config_entry(
|
||||
let root = channel_root?;
|
||||
let account = account_id.map(str::trim).filter(|s| !s.is_empty());
|
||||
if let Some(acct) = account {
|
||||
if platform_storage_key(platform) == "tlon" && acct == QQBOT_DEFAULT_ACCOUNT_ID {
|
||||
return Some(root.clone());
|
||||
}
|
||||
if let Some(value) = root.get("accounts").and_then(|a| a.get(acct)) {
|
||||
return Some(value.clone());
|
||||
}
|
||||
@@ -2094,6 +2108,41 @@ pub async fn read_platform_config(
|
||||
}
|
||||
}
|
||||
}
|
||||
"tlon" => {
|
||||
let mut shared = channel_root
|
||||
.and_then(|root| root.as_object())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if let Some(saved_obj) = saved.as_object() {
|
||||
for (key, value) in saved_obj {
|
||||
shared.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
let shared = Value::Object(shared);
|
||||
for key in ["name", "ship", "url", "code", "responsePrefix", "ownerShip"] {
|
||||
insert_secret_aware_form_value(&mut form, &shared, key);
|
||||
}
|
||||
insert_bool_as_string(&mut form, &shared, "enabled");
|
||||
if let Some(network) = shared.get("network") {
|
||||
insert_bool_as_string(&mut form, network, "dangerouslyAllowPrivateNetwork");
|
||||
}
|
||||
for key in [
|
||||
"groupChannels",
|
||||
"dmAllowlist",
|
||||
"groupInviteAllowlist",
|
||||
"defaultAuthorizedShips",
|
||||
] {
|
||||
insert_array_as_csv(&mut form, &shared, key);
|
||||
}
|
||||
for key in [
|
||||
"autoDiscoverChannels",
|
||||
"showModelSignature",
|
||||
"autoAcceptDmInvites",
|
||||
"autoAcceptGroupInvites",
|
||||
] {
|
||||
insert_bool_as_string(&mut form, &shared, key);
|
||||
}
|
||||
}
|
||||
"synology-chat" => {
|
||||
for key in ["token", "incomingUrl", "nasHost", "webhookPath", "botName"] {
|
||||
insert_secret_aware_form_value(&mut form, &saved, key);
|
||||
@@ -3445,6 +3494,75 @@ pub async fn save_messaging_platform(
|
||||
)?;
|
||||
ensure_plugin_allowed(&mut cfg, "irc")?;
|
||||
}
|
||||
"tlon" => {
|
||||
let ship = form_string(form_obj, "ship");
|
||||
let url = form_string(form_obj, "url");
|
||||
let code = form_string(form_obj, "code");
|
||||
if ship.is_empty() {
|
||||
return Err("Tlon Ship 不能为空".into());
|
||||
}
|
||||
if url.is_empty() {
|
||||
return Err("Tlon URL 不能为空".into());
|
||||
}
|
||||
if code.is_empty() && !has_configured_messaging_value(form_obj.get("code")) {
|
||||
return Err("Tlon Code 不能为空".into());
|
||||
}
|
||||
|
||||
let mut entry = Map::new();
|
||||
entry.insert("enabled".into(), Value::Bool(true));
|
||||
put_bool_value_if_present(&mut entry, "enabled", form_obj.get("enabled"));
|
||||
for key in ["name", "ship", "url", "responsePrefix", "ownerShip"] {
|
||||
put_string(&mut entry, key, form_string(form_obj, key));
|
||||
}
|
||||
match resolve_messaging_credential_value_for_save(form_obj, ¤t_saved, "code") {
|
||||
Some(value) => {
|
||||
entry.insert("code".into(), value);
|
||||
}
|
||||
None => {
|
||||
entry.remove("code");
|
||||
}
|
||||
}
|
||||
for key in [
|
||||
"groupChannels",
|
||||
"dmAllowlist",
|
||||
"groupInviteAllowlist",
|
||||
"defaultAuthorizedShips",
|
||||
] {
|
||||
put_array_from_form_value(&mut entry, key, form_obj.get(key));
|
||||
}
|
||||
for key in [
|
||||
"autoDiscoverChannels",
|
||||
"showModelSignature",
|
||||
"autoAcceptDmInvites",
|
||||
"autoAcceptGroupInvites",
|
||||
] {
|
||||
put_bool_value_if_present(&mut entry, key, form_obj.get(key));
|
||||
}
|
||||
if form_obj.contains_key("dangerouslyAllowPrivateNetwork") {
|
||||
let mut network = current_saved
|
||||
.get("network")
|
||||
.and_then(|v| v.as_object())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
put_bool_value_if_present(
|
||||
&mut network,
|
||||
"dangerouslyAllowPrivateNetwork",
|
||||
form_obj.get("dangerouslyAllowPrivateNetwork"),
|
||||
);
|
||||
if !network.is_empty() {
|
||||
entry.insert("network".into(), Value::Object(network));
|
||||
}
|
||||
}
|
||||
preserve_messaging_credential_refs(&mut entry, form_obj, ¤t_saved);
|
||||
let target_account_id =
|
||||
if account_id.as_deref().map(str::trim) == Some(QQBOT_DEFAULT_ACCOUNT_ID) {
|
||||
None
|
||||
} else {
|
||||
account_id.as_deref()
|
||||
};
|
||||
merge_channel_entry_for_account(channels_map, &storage_key, target_account_id, entry)?;
|
||||
ensure_plugin_allowed(&mut cfg, "tlon")?;
|
||||
}
|
||||
"synology-chat" => {
|
||||
let token = form_string(form_obj, "token");
|
||||
let incoming_url = form_string(form_obj, "incomingUrl");
|
||||
@@ -3762,6 +3880,10 @@ pub async fn verify_bot_token(platform: String, form: Value) -> Result<Value, St
|
||||
"valid": true,
|
||||
"warnings": ["IRC 面板已完成基础字段校验;实际连通性请通过 Gateway 启动日志或 openclaw channels status --probe 验证"]
|
||||
})),
|
||||
"tlon" => Ok(json!({
|
||||
"valid": true,
|
||||
"warnings": ["Tlon 面板已完成基础字段校验;实际连通性请通过 Gateway 启动日志或 openclaw channels status --probe 验证"]
|
||||
})),
|
||||
_ => Ok(json!({
|
||||
"valid": true,
|
||||
"warnings": ["该平台暂不支持在线校验"]
|
||||
@@ -4686,6 +4808,7 @@ pub async fn list_configured_platforms() -> Result<Value, String> {
|
||||
.or_else(|| account_display_value(acct_val, "clientId"))
|
||||
.or_else(|| account_display_value(acct_val, "account"))
|
||||
.or_else(|| account_display_value(acct_val, "nick"))
|
||||
.or_else(|| account_display_value(acct_val, "ship"))
|
||||
{
|
||||
entry["appId"] = Value::String(display_id);
|
||||
}
|
||||
@@ -7296,6 +7419,139 @@ mod tests {
|
||||
.contains("IRC 面板已完成基础字段校验"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_tlon_form_preserves_ship_login_and_invite_fields() {
|
||||
let form = json!({
|
||||
"enabled": "true",
|
||||
"name": "Main Ship",
|
||||
"ship": "~sampel-palnet",
|
||||
"url": "https://urbit.example.com",
|
||||
"code": "lidlut-tabwed-pillex-ridrup",
|
||||
"dangerouslyAllowPrivateNetwork": "true",
|
||||
"groupChannels": "chat/~host-ship/general, chat/~host-ship/support",
|
||||
"dmAllowlist": "zod, ~nec",
|
||||
"groupInviteAllowlist": "~bus",
|
||||
"autoDiscoverChannels": "true",
|
||||
"showModelSignature": "false",
|
||||
"responsePrefix": "[Tlon]",
|
||||
"autoAcceptDmInvites": "true",
|
||||
"autoAcceptGroupInvites": "false",
|
||||
"ownerShip": "~sampel-palnet",
|
||||
"defaultAuthorizedShips": "~zod, ~nec"
|
||||
});
|
||||
let normalized =
|
||||
normalize_messaging_platform_form("tlon", form.as_object().expect("object"));
|
||||
|
||||
assert_eq!(
|
||||
normalized.get("enabled").and_then(|v| v.as_bool()),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
normalized
|
||||
.get("dangerouslyAllowPrivateNetwork")
|
||||
.and_then(|v| v.as_bool()),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
normalized
|
||||
.get("groupChannels")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|items| items.len()),
|
||||
Some(2)
|
||||
);
|
||||
assert_eq!(
|
||||
normalized
|
||||
.get("dmAllowlist")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|items| items.len()),
|
||||
Some(2)
|
||||
);
|
||||
assert_eq!(
|
||||
normalized
|
||||
.get("groupInviteAllowlist")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|items| items.len()),
|
||||
Some(1)
|
||||
);
|
||||
assert_eq!(
|
||||
normalized
|
||||
.get("defaultAuthorizedShips")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|items| items.len()),
|
||||
Some(2)
|
||||
);
|
||||
assert_eq!(
|
||||
normalized
|
||||
.get("autoDiscoverChannels")
|
||||
.and_then(|v| v.as_bool()),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
normalized
|
||||
.get("showModelSignature")
|
||||
.and_then(|v| v.as_bool()),
|
||||
Some(false)
|
||||
);
|
||||
assert_eq!(
|
||||
normalized
|
||||
.get("autoAcceptDmInvites")
|
||||
.and_then(|v| v.as_bool()),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
normalized
|
||||
.get("autoAcceptGroupInvites")
|
||||
.and_then(|v| v.as_bool()),
|
||||
Some(false)
|
||||
);
|
||||
assert!(channel_diagnosis_credentials_ready("tlon", &normalized));
|
||||
|
||||
let missing = normalize_messaging_platform_form(
|
||||
"tlon",
|
||||
json!({
|
||||
"ship": "~sampel-palnet",
|
||||
"url": "https://urbit.example.com"
|
||||
})
|
||||
.as_object()
|
||||
.expect("object"),
|
||||
);
|
||||
assert!(!channel_diagnosis_credentials_ready("tlon", &missing));
|
||||
let diagnosis =
|
||||
build_openclaw_channel_diagnosis("tlon", None, true, true, &missing, None, None);
|
||||
assert!(diagnosis
|
||||
.get("checks")
|
||||
.and_then(|v| v.as_array())
|
||||
.and_then(|items| items
|
||||
.iter()
|
||||
.find(|item| { item.get("id").and_then(|v| v.as_str()) == Some("credentials") }))
|
||||
.and_then(|item| item.get("detail"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.contains("Code"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_tlon_token_returns_probe_guidance_warning() {
|
||||
let result = tauri::async_runtime::block_on(verify_bot_token(
|
||||
"tlon".to_string(),
|
||||
json!({
|
||||
"ship": "~sampel-palnet",
|
||||
"url": "https://urbit.example.com",
|
||||
"code": "lidlut-tabwed-pillex-ridrup"
|
||||
}),
|
||||
))
|
||||
.expect("verify result");
|
||||
|
||||
assert_eq!(result.get("valid").and_then(|v| v.as_bool()), Some(true));
|
||||
assert!(result
|
||||
.get("warnings")
|
||||
.and_then(|v| v.as_array())
|
||||
.and_then(|items| items.first())
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.contains("Tlon 面板已完成基础字段校验"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_form_readback_preserves_mention_policy_choice() {
|
||||
let saved = json!({
|
||||
|
||||
@@ -18,6 +18,7 @@ export const CHANNEL_LABELS = {
|
||||
imessage: 'iMessage',
|
||||
line: 'LINE',
|
||||
nostr: 'Nostr',
|
||||
tlon: 'Tlon',
|
||||
mattermost: 'Mattermost',
|
||||
clickclack: 'ClickClack',
|
||||
'nextcloud-talk': 'Nextcloud Talk',
|
||||
|
||||
@@ -190,6 +190,28 @@ export default {
|
||||
nostrDefaultAccountHint: _('上游当前通过根节点配置生成隐式账号,通常保持 default 即可。', 'Upstream currently derives an implicit account from the root config; default is usually enough.'),
|
||||
nostrProfileAboutPh: _('可选,展示在 Nostr Profile 中的机器人介绍', 'Optional bot introduction shown in the Nostr profile'),
|
||||
nostrProfileUrlHint: _('上游 Profile URL 字段要求使用 https:// 地址。', 'Upstream profile URL fields require https:// URLs.'),
|
||||
tlonDesc: _('接入 Tlon / Urbit 消息网络,支持 Ship 登录、群组频道、私信白名单和邀请控制', 'Connect Tlon / Urbit messaging with ship login, group channels, DM allowlists, and invite controls'),
|
||||
tlonGuide1: _('准备可访问的 Tlon Ship,例如 <code>~sampel-palnet</code>,并确认 Gateway 网络可以访问该 Ship 的 URL', 'Prepare a reachable Tlon ship, for example <code>~sampel-palnet</code>, and make sure Gateway can access its URL'),
|
||||
tlonGuide2: _('填写 Ship、URL 和登录 Code;这是上游 Tlon 插件的最小可运行配置', 'Fill Ship, URL, and login Code; these are the minimum fields required by the upstream Tlon plugin'),
|
||||
tlonGuide3: _('按需填写 Group Channels、DM Allowlist 与 Group Invite Allowlist,限制机器人可响应和可接受邀请的范围', 'Set Group Channels, DM Allowlist, and Group Invite Allowlist as needed to limit where the bot can respond and accept invites'),
|
||||
tlonGuide4: _('保存后面板会启用 Tlon 插件并重载 Gateway;真实连通性以 Gateway 日志或 channels status --probe 为准', 'After saving, the panel enables the Tlon plugin and reloads Gateway; verify connectivity through Gateway logs or channels status --probe'),
|
||||
tlonGuideFooter: _('<div style="margin-top:8px;font-size:var(--font-size-xs);color:var(--text-tertiary)">Tlon 最小配置需要 Ship、URL 与 Code;命名账号会写入 <code>channels.tlon.accounts</code>,默认账号写入 <code>channels.tlon</code> 根节点。</div>', '<div style="margin-top:8px;font-size:var(--font-size-xs);color:var(--text-tertiary)">Tlon minimally requires Ship, URL, and Code; named accounts are written to <code>channels.tlon.accounts</code>, while the default account is written to the <code>channels.tlon</code> root.</div>'),
|
||||
tlonShipHint: _('Ship 名通常以 ~ 开头,例如 ~sampel-palnet。', 'Ship names usually start with ~, for example ~sampel-palnet.'),
|
||||
tlonUrlHint: _('Tlon 实例 URL,建议使用 https://。如为内网地址,需确认 Gateway 可访问。', 'Tlon instance URL; https:// is recommended. For private addresses, make sure Gateway can reach it.'),
|
||||
tlonCodeHint: _('Tlon 登录码;生产环境建议使用 SecretRef,保持 SecretRef 占位不变即可保留引用。', 'Tlon login code; prefer SecretRef in production. Keep the SecretRef placeholder unchanged to preserve the reference.'),
|
||||
tlonPrivateNetworkHint: _('仅在 Tlon URL 是可信内网地址时开启;该开关会写入 network.dangerouslyAllowPrivateNetwork。', 'Enable only for trusted private Tlon URLs; this writes network.dangerouslyAllowPrivateNetwork.'),
|
||||
tlonGroupChannelsHint: _('群组频道 Nest,多个值用逗号分隔,例如 chat/~host-ship/general。', 'Group channel nests separated by commas, for example chat/~host-ship/general.'),
|
||||
tlonDmAllowlistHint: _('允许发起私信的 Ship 列表,多个值用逗号分隔。', 'Ships allowed to start DMs, separated by commas.'),
|
||||
tlonGroupInviteAllowlistHint: _('允许邀请机器人加入群组的 Ship 列表,留空表示按上游默认处理。', 'Ships allowed to invite the bot to groups; leave empty to use upstream defaults.'),
|
||||
tlonAutoDiscoverChannels: _('自动发现频道', 'Auto-discover channels'),
|
||||
tlonAutoDiscoverChannelsHint: _('开启后上游会尝试发现 Ship 可用频道;大型 Ship 上建议先显式填写 Group Channels。', 'When enabled, upstream tries to discover available channels; for large ships, explicitly setting Group Channels first is recommended.'),
|
||||
tlonShowModelSignature: _('显示模型签名', 'Show model signature'),
|
||||
tlonAutoAcceptDmInvites: _('自动接受私信邀请', 'Auto-accept DM invites'),
|
||||
tlonAutoAcceptDmInvitesHint: _('通常仅配合 DM Allowlist 使用,避免接受未知 Ship。', 'Usually use this with DM Allowlist to avoid accepting unknown ships.'),
|
||||
tlonAutoAcceptGroupInvites: _('自动接受群组邀请', 'Auto-accept group invites'),
|
||||
tlonAutoAcceptGroupInvitesHint: _('建议同时配置 Group Invite Allowlist,避免恶意群组邀请。', 'Configure Group Invite Allowlist as well to avoid malicious group invites.'),
|
||||
tlonOwnerShipHint: _('用于审批请求的 Owner Ship,可填写当前 Ship 或专门的管理 Ship。', 'Owner ship that receives approval requests; use the current ship or a dedicated management ship.'),
|
||||
tlonDefaultAuthorizedShipsHint: _('默认授权 Ship 列表,多个值用逗号分隔。', 'Default authorized ships separated by commas.'),
|
||||
ircDesc: _('接入 IRC 网络,支持服务器账号、NickServ 登录、频道白名单和提及策略', 'Connect IRC networks with server accounts, NickServ login, channel allowlists, and mention policy'),
|
||||
ircGuide1: _('准备 IRC 服务器地址,例如 <code>irc.libera.chat</code>,并确认 Gateway 网络可以连接', 'Prepare the IRC server host, for example <code>irc.libera.chat</code>, and make sure Gateway can connect'),
|
||||
ircGuide2: _('填写机器人 Nick;如服务器需要 SASL 或密码,可填写 Server Password 或文件路径', 'Fill the bot Nick; if the server requires SASL or a password, provide Server Password or a file path'),
|
||||
|
||||
@@ -418,6 +418,39 @@ const PLATFORM_REGISTRY = {
|
||||
pluginRequired: '@openclaw/nostr@latest',
|
||||
pluginId: 'nostr',
|
||||
},
|
||||
tlon: {
|
||||
label: 'Tlon',
|
||||
iconName: 'globe',
|
||||
desc: t('channels.tlonDesc'),
|
||||
guide: [
|
||||
t('channels.tlonGuide1'),
|
||||
t('channels.tlonGuide2'),
|
||||
t('channels.tlonGuide3'),
|
||||
t('channels.tlonGuide4'),
|
||||
],
|
||||
guideFooter: t('channels.tlonGuideFooter'),
|
||||
fields: [
|
||||
{ key: 'name', label: t('channels.accountName'), placeholder: t('channels.optionalEg', { example: 'main-ship' }), required: false },
|
||||
{ key: 'ship', label: 'Ship', placeholder: '~sampel-palnet', required: true, hint: t('channels.tlonShipHint') },
|
||||
{ key: 'url', label: 'URL', placeholder: 'https://urbit.example.com', required: true, hint: t('channels.tlonUrlHint') },
|
||||
{ key: 'code', label: 'Code', placeholder: 'lidlut-tabwed-pillex-ridrup', secret: true, required: true, hint: t('channels.tlonCodeHint') },
|
||||
{ key: 'dangerouslyAllowPrivateNetwork', label: t('channels.mattermostPrivateNetwork'), type: 'select', options: BOOLEAN_OPTIONS, required: false, hint: t('channels.tlonPrivateNetworkHint') },
|
||||
{ key: 'groupChannels', label: 'Group Channels', placeholder: 'chat/~host-ship/general, chat/~host-ship/support', required: false, hint: t('channels.tlonGroupChannelsHint') },
|
||||
{ key: 'dmAllowlist', label: 'DM Allowlist', placeholder: '~zod, ~nec', required: false, hint: t('channels.tlonDmAllowlistHint') },
|
||||
{ key: 'groupInviteAllowlist', label: 'Group Invite Allowlist', placeholder: '~zod, ~nec', required: false, hint: t('channels.tlonGroupInviteAllowlistHint') },
|
||||
{ key: 'autoDiscoverChannels', label: t('channels.tlonAutoDiscoverChannels'), type: 'select', options: BOOLEAN_OPTIONS, required: false, hint: t('channels.tlonAutoDiscoverChannelsHint') },
|
||||
{ key: 'showModelSignature', label: t('channels.tlonShowModelSignature'), type: 'select', options: BOOLEAN_OPTIONS, required: false },
|
||||
{ key: 'responsePrefix', label: 'Response Prefix', placeholder: t('channels.optionalEg', { example: '[Tlon]' }), required: false },
|
||||
{ key: 'autoAcceptDmInvites', label: t('channels.tlonAutoAcceptDmInvites'), type: 'select', options: BOOLEAN_OPTIONS, required: false, hint: t('channels.tlonAutoAcceptDmInvitesHint') },
|
||||
{ key: 'autoAcceptGroupInvites', label: t('channels.tlonAutoAcceptGroupInvites'), type: 'select', options: BOOLEAN_OPTIONS, required: false, hint: t('channels.tlonAutoAcceptGroupInvitesHint') },
|
||||
{ key: 'ownerShip', label: 'Owner Ship', placeholder: '~sampel-palnet', required: false, hint: t('channels.tlonOwnerShipHint') },
|
||||
{ key: 'defaultAuthorizedShips', label: 'Default Authorized Ships', placeholder: '~zod, ~nec', required: false, hint: t('channels.tlonDefaultAuthorizedShipsHint') },
|
||||
],
|
||||
configKey: 'tlon',
|
||||
pairingChannel: 'tlon',
|
||||
pluginRequired: '@openclaw/tlon@latest',
|
||||
pluginId: 'tlon',
|
||||
},
|
||||
irc: {
|
||||
label: 'IRC',
|
||||
iconName: 'hash',
|
||||
@@ -993,7 +1026,7 @@ function applyRouteIntent(page, state) {
|
||||
// ── 已配置平台渲染 ──
|
||||
|
||||
// ── 多账号支持的平台:与 OpenClaw 的 accounts/defaultAccount 配置模型保持一致 ──
|
||||
const MULTI_INSTANCE_PLATFORMS = ['telegram', 'discord', 'slack', 'feishu', 'dingtalk', 'dingtalk-connector', 'qqbot', 'zalo', 'zalouser', 'line', 'mattermost', 'clickclack', 'nextcloud-talk', 'twitch', 'irc', 'synology-chat', 'googlechat', 'signal']
|
||||
const MULTI_INSTANCE_PLATFORMS = ['telegram', 'discord', 'slack', 'feishu', 'dingtalk', 'dingtalk-connector', 'qqbot', 'zalo', 'zalouser', 'line', 'mattermost', 'clickclack', 'nextcloud-talk', 'twitch', 'tlon', 'irc', 'synology-chat', 'googlechat', 'signal']
|
||||
|
||||
function supportsMessagingMultiAccount(pid) {
|
||||
return MULTI_INSTANCE_PLATFORMS.includes(pid)
|
||||
|
||||
@@ -1149,6 +1149,18 @@ mark {
|
||||
|
||||
/* === 移动端响应式 === */
|
||||
@media (max-width: 768px) {
|
||||
.modal .btn {
|
||||
min-height: 44px;
|
||||
justify-content: center;
|
||||
}
|
||||
.modal .btn-sm,
|
||||
.modal .btn-xs {
|
||||
min-height: 40px;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
}
|
||||
.modal-actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.stat-cards {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--space-sm);
|
||||
|
||||
@@ -689,6 +689,145 @@ test('IRC 读取和诊断会回显服务器、NickServ 与频道字段', () => {
|
||||
assert.equal(ready.checks.find(item => item.id === 'credentials')?.ok, true)
|
||||
})
|
||||
|
||||
test('Tlon 默认账号保存会写入上游根节点配置并启用插件', () => {
|
||||
const cfg = { channels: {} }
|
||||
|
||||
mergeOpenClawMessagingPlatformConfig(cfg, {
|
||||
platform: 'tlon',
|
||||
accountId: 'default',
|
||||
form: {
|
||||
enabled: 'true',
|
||||
name: 'Main Ship',
|
||||
ship: '~sampel-palnet',
|
||||
url: 'https://urbit.example.com',
|
||||
code: 'lidlut-tabwed-pillex-ridrup',
|
||||
dangerouslyAllowPrivateNetwork: 'true',
|
||||
groupChannels: 'chat/~host-ship/general, chat/~host-ship/support',
|
||||
dmAllowlist: 'zod, ~nec',
|
||||
groupInviteAllowlist: '~bus',
|
||||
autoDiscoverChannels: 'true',
|
||||
showModelSignature: 'false',
|
||||
responsePrefix: '[Tlon]',
|
||||
autoAcceptDmInvites: 'true',
|
||||
autoAcceptGroupInvites: 'false',
|
||||
ownerShip: '~sampel-palnet',
|
||||
defaultAuthorizedShips: '~zod, ~nec',
|
||||
},
|
||||
})
|
||||
|
||||
const root = cfg.channels.tlon
|
||||
assert.equal(root.enabled, true)
|
||||
assert.equal(root.name, 'Main Ship')
|
||||
assert.equal(root.ship, '~sampel-palnet')
|
||||
assert.equal(root.url, 'https://urbit.example.com')
|
||||
assert.equal(root.code, 'lidlut-tabwed-pillex-ridrup')
|
||||
assert.deepEqual(root.network, { dangerouslyAllowPrivateNetwork: true })
|
||||
assert.deepEqual(root.groupChannels, ['chat/~host-ship/general', 'chat/~host-ship/support'])
|
||||
assert.deepEqual(root.dmAllowlist, ['zod', '~nec'])
|
||||
assert.deepEqual(root.groupInviteAllowlist, ['~bus'])
|
||||
assert.equal(root.autoDiscoverChannels, true)
|
||||
assert.equal(root.showModelSignature, false)
|
||||
assert.equal(root.responsePrefix, '[Tlon]')
|
||||
assert.equal(root.autoAcceptDmInvites, true)
|
||||
assert.equal(root.autoAcceptGroupInvites, false)
|
||||
assert.equal(root.ownerShip, '~sampel-palnet')
|
||||
assert.deepEqual(root.defaultAuthorizedShips, ['~zod', '~nec'])
|
||||
assert.equal(Object.hasOwn(root, 'accounts'), false)
|
||||
assert.equal(cfg.plugins.entries.tlon.enabled, true)
|
||||
})
|
||||
|
||||
test('Tlon 命名账号保存会写入 accounts 并保留根节点共享字段', () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
tlon: {
|
||||
enabled: true,
|
||||
defaultAuthorizedShips: ['~zod'],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mergeOpenClawMessagingPlatformConfig(cfg, {
|
||||
platform: 'tlon',
|
||||
accountId: 'support',
|
||||
form: {
|
||||
enabled: 'true',
|
||||
ship: '~support-palnet',
|
||||
url: 'https://support.example.com',
|
||||
code: 'fodwyt-ragful-sivnys-nivlup',
|
||||
groupChannels: 'chat/~host-ship/support',
|
||||
dmAllowlist: '~zod',
|
||||
autoDiscoverChannels: 'false',
|
||||
ownerShip: '~support-palnet',
|
||||
},
|
||||
})
|
||||
|
||||
const root = cfg.channels.tlon
|
||||
const account = root.accounts.support
|
||||
assert.deepEqual(root.defaultAuthorizedShips, ['~zod'])
|
||||
assert.equal(account.enabled, true)
|
||||
assert.equal(account.ship, '~support-palnet')
|
||||
assert.equal(account.url, 'https://support.example.com')
|
||||
assert.equal(account.code, 'fodwyt-ragful-sivnys-nivlup')
|
||||
assert.deepEqual(account.groupChannels, ['chat/~host-ship/support'])
|
||||
assert.deepEqual(account.dmAllowlist, ['~zod'])
|
||||
assert.equal(account.autoDiscoverChannels, false)
|
||||
assert.equal(account.ownerShip, '~support-palnet')
|
||||
assert.equal(cfg.plugins.entries.tlon.enabled, true)
|
||||
})
|
||||
|
||||
test('Tlon 读取和诊断会回显 Ship、URL、登录码和安全配置', () => {
|
||||
const values = buildMessagingPlatformFormValues('tlon', {
|
||||
enabled: true,
|
||||
name: 'Main Ship',
|
||||
ship: '~sampel-palnet',
|
||||
url: 'https://urbit.example.com',
|
||||
code: 'lidlut-tabwed-pillex-ridrup',
|
||||
network: { dangerouslyAllowPrivateNetwork: true },
|
||||
groupChannels: ['chat/~host-ship/general', 'chat/~host-ship/support'],
|
||||
dmAllowlist: ['~zod', '~nec'],
|
||||
groupInviteAllowlist: ['~bus'],
|
||||
autoDiscoverChannels: true,
|
||||
showModelSignature: false,
|
||||
responsePrefix: '[Tlon]',
|
||||
autoAcceptDmInvites: true,
|
||||
autoAcceptGroupInvites: false,
|
||||
ownerShip: '~sampel-palnet',
|
||||
defaultAuthorizedShips: ['~zod', '~nec'],
|
||||
})
|
||||
const missingCode = buildOpenClawChannelDiagnosis({
|
||||
platform: 'tlon',
|
||||
configExists: true,
|
||||
channelEnabled: true,
|
||||
form: { ship: '~sampel-palnet', url: 'https://urbit.example.com' },
|
||||
})
|
||||
const ready = buildOpenClawChannelDiagnosis({
|
||||
platform: 'tlon',
|
||||
configExists: true,
|
||||
channelEnabled: true,
|
||||
form: values,
|
||||
})
|
||||
|
||||
assert.equal(values.enabled, 'true')
|
||||
assert.equal(values.name, 'Main Ship')
|
||||
assert.equal(values.ship, '~sampel-palnet')
|
||||
assert.equal(values.url, 'https://urbit.example.com')
|
||||
assert.equal(values.code, 'lidlut-tabwed-pillex-ridrup')
|
||||
assert.equal(values.dangerouslyAllowPrivateNetwork, 'true')
|
||||
assert.equal(values.groupChannels, 'chat/~host-ship/general, chat/~host-ship/support')
|
||||
assert.equal(values.dmAllowlist, '~zod, ~nec')
|
||||
assert.equal(values.groupInviteAllowlist, '~bus')
|
||||
assert.equal(values.autoDiscoverChannels, 'true')
|
||||
assert.equal(values.showModelSignature, 'false')
|
||||
assert.equal(values.responsePrefix, '[Tlon]')
|
||||
assert.equal(values.autoAcceptDmInvites, 'true')
|
||||
assert.equal(values.autoAcceptGroupInvites, 'false')
|
||||
assert.equal(values.ownerShip, '~sampel-palnet')
|
||||
assert.equal(values.defaultAuthorizedShips, '~zod, ~nec')
|
||||
assert.equal(missingCode.checks.find(item => item.id === 'credentials')?.ok, false)
|
||||
assert.match(missingCode.checks.find(item => item.id === 'credentials')?.detail || '', /Code/)
|
||||
assert.equal(ready.checks.find(item => item.id === 'credentials')?.ok, true)
|
||||
})
|
||||
|
||||
test('Signal 渠道保存会保留多账号和上游运行字段', () => {
|
||||
const cfg = { channels: {} }
|
||||
|
||||
@@ -943,6 +1082,20 @@ test('渠道账号列表会使用 IRC Nick 作为安全展示标识', () => {
|
||||
])
|
||||
})
|
||||
|
||||
test('渠道账号列表会使用 Tlon Ship 作为安全展示标识', () => {
|
||||
const accounts = listPlatformAccounts({
|
||||
accounts: {
|
||||
support: {
|
||||
ship: '~support-palnet',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
assert.deepEqual(accounts, [
|
||||
{ accountId: 'support', appId: '~support-palnet' },
|
||||
])
|
||||
})
|
||||
|
||||
test('渠道保存时 clientId 未改动 SecretRef 占位会保留原始引用', () => {
|
||||
const secretRef = { source: 'env', provider: 'default', id: 'DINGTALK_CLIENT_ID' }
|
||||
const value = resolveMessagingCredentialValueForSave({
|
||||
|
||||
@@ -197,3 +197,28 @@ test('IRC 渠道 UI 会暴露服务器、NickServ 与频道访问配置字段',
|
||||
assert.match(ircBlock, /pluginRequired:\s*'@openclaw\/irc@latest'/)
|
||||
assert.match(ircBlock, /pluginId:\s*'irc'/)
|
||||
})
|
||||
|
||||
test('Tlon 渠道 UI 会暴露 Urbit 登录、频道和邀请安全配置字段', () => {
|
||||
const tlonBlock = getRegistryBlock('tlon')
|
||||
|
||||
for (const field of [
|
||||
'ship',
|
||||
'url',
|
||||
'code',
|
||||
'dangerouslyAllowPrivateNetwork',
|
||||
'groupChannels',
|
||||
'dmAllowlist',
|
||||
'groupInviteAllowlist',
|
||||
'autoDiscoverChannels',
|
||||
'showModelSignature',
|
||||
'responsePrefix',
|
||||
'autoAcceptDmInvites',
|
||||
'autoAcceptGroupInvites',
|
||||
'ownerShip',
|
||||
'defaultAuthorizedShips',
|
||||
]) {
|
||||
assert.match(tlonBlock, new RegExp(`key:\\s*'${field}'`))
|
||||
}
|
||||
assert.match(tlonBlock, /pluginRequired:\s*'@openclaw\/tlon@latest'/)
|
||||
assert.match(tlonBlock, /pluginId:\s*'tlon'/)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user