diff --git a/scripts/dev-api.js b/scripts/dev-api.js index 8592f29..f605b31 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -2472,7 +2472,7 @@ export function normalizeMessagingPlatformForm(platform, form = {}) { normalized.allowedUserIds = csvToStringArray(normalized.allowedUserIds) } - for (const key of ['promptStarters', 'delegatedAuthScopes', 'attachmentRoots', 'remoteAttachmentRoots', 'toolsAllow', 'allowedRoles']) { + for (const key of ['promptStarters', 'delegatedAuthScopes', 'attachmentRoots', 'remoteAttachmentRoots', 'toolsAllow', 'allowedRoles', 'relays']) { if (Object.hasOwn(normalized, key)) normalized[key] = csvToStringArray(normalized[key]) } @@ -2607,6 +2607,7 @@ const MESSAGING_CREDENTIAL_FIELDS = [ 'gatewayPassword', 'gatewayToken', 'password', + 'privateKey', 'secretFile', 'serviceAccount', 'serviceAccountFile', @@ -2696,6 +2697,7 @@ const CHANNEL_DIAG_REQUIRED_FIELDS = { 'synology-chat': [['token', 'Token'], ['incomingUrl', 'Incoming URL']], clickclack: [['baseUrl', 'Base URL'], ['token', 'Token'], ['workspace', 'Workspace']], 'nextcloud-talk': [['baseUrl', 'Base URL']], + nostr: [['privateKey', 'Private Key']], twitch: [['username', 'Username'], ['accessToken', 'Access Token'], ['clientId', 'Client ID'], ['channel', 'Channel']], signal: [['account', 'Signal 账号']], } @@ -3106,6 +3108,31 @@ export function buildMessagingPlatformFormValues(platform, saved = {}, options = return form } + if (storageKey === 'nostr') { + putSecretAwareFormValue(form, saved, 'privateKey') + for (const key of ['name', 'defaultAccount', 'dmPolicy']) { + putStringFormValue(form, saved, key) + } + putBoolFormValue(form, saved, 'enabled') + putCsvFormValue(form, saved, 'relays') + putCsvFormValue(form, saved, 'allowFrom') + const profile = saved.profile && typeof saved.profile === 'object' ? saved.profile : {} + const profileMap = { + name: 'profileName', + displayName: 'profileDisplayName', + about: 'profileAbout', + picture: 'profilePicture', + banner: 'profileBanner', + website: 'profileWebsite', + nip05: 'profileNip05', + lud16: 'profileLud16', + } + for (const [sourceKey, formKey] of Object.entries(profileMap)) { + if (typeof profile[sourceKey] === 'string') form[formKey] = profile[sourceKey] + } + return form + } + if (storageKey === 'synology-chat') { for (const key of ['token', 'incomingUrl', 'nasHost', 'webhookPath', 'botName']) { putSecretAwareFormValue(form, saved, key) @@ -3898,6 +3925,28 @@ function buildOpenClawMessagingPlatformEntry(platform, form, currentSaved = {}) for (const key of ['expiresIn', 'obtainmentTimestamp']) { if (typeof form[key] === 'number') entry[key] = form[key] } + } else if (storageKey === 'nostr') { + entry.enabled = typeof form.enabled === 'boolean' ? form.enabled : true + for (const key of ['name', 'defaultAccount', 'privateKey', 'dmPolicy']) { + if (form[key]) entry[key] = form[key] + } + if (Array.isArray(form.relays) && form.relays.length) entry.relays = form.relays + if (Array.isArray(form.allowFrom) && form.allowFrom.length) entry.allowFrom = form.allowFrom + const profileMap = { + profileName: 'name', + profileDisplayName: 'displayName', + profileAbout: 'about', + profilePicture: 'picture', + profileBanner: 'banner', + profileWebsite: 'website', + profileNip05: 'nip05', + profileLud16: 'lud16', + } + const profile = {} + for (const [formKey, targetKey] of Object.entries(profileMap)) { + if (form[formKey]) profile[targetKey] = form[formKey] + } + if (Object.keys(profile).length) entry.profile = profile } else if (storageKey === 'synology-chat') { for (const key of ['token', 'incomingUrl', 'nasHost', 'webhookPath', 'botName']) { if (form[key]) entry[key] = form[key] @@ -3938,8 +3987,8 @@ 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, normalizedAccountId, entry) - if (['zalo', 'zalouser', 'line', 'mattermost', 'clickclack', 'nextcloud-talk', 'twitch', 'synology-chat', 'googlechat', 'msteams', 'imessage', 'whatsapp'].includes(storageKey)) { + applyMessagingPlatformEntry(cfg, storageKey, storageKey === 'nostr' ? '' : normalizedAccountId, entry) + if (['zalo', 'zalouser', 'line', 'mattermost', 'clickclack', 'nextcloud-talk', 'twitch', 'nostr', 'synology-chat', 'googlechat', 'msteams', 'imessage', 'whatsapp'].includes(storageKey)) { ensureMessagingPluginAllowed(cfg, storageKey) } return { entry, accountId: normalizedAccountId, storageKey } @@ -5409,16 +5458,16 @@ const handlers = { } else { setRootChannelEntry(entry) } - } else if (['line', 'mattermost', 'clickclack', 'nextcloud-talk', 'twitch', 'synology-chat', 'googlechat', 'msteams', 'whatsapp'].includes(storageKey)) { + } else if (['line', 'mattermost', 'clickclack', 'nextcloud-talk', 'twitch', 'nostr', 'synology-chat', 'googlechat', 'msteams', 'whatsapp'].includes(storageKey)) { const built = buildOpenClawMessagingPlatformEntry(platform, form, currentSaved) - applyMessagingPlatformEntry(cfg, storageKey, normalizedAccountId, built) + applyMessagingPlatformEntry(cfg, storageKey, storageKey === 'nostr' ? '' : normalizedAccountId, 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', 'synology-chat', 'googlechat', 'msteams', 'whatsapp'].includes(storageKey)) { + if (platform !== 'qqbot' && platform !== 'feishu' && platform !== 'dingtalk' && platform !== 'dingtalk-connector' && !['line', 'mattermost', 'clickclack', 'nextcloud-talk', 'twitch', 'nostr', 'synology-chat', 'googlechat', 'msteams', 'whatsapp'].includes(storageKey)) { preserveMessagingCredentialRefs(entry, form, currentSaved) // 合并模式:保留用户通过 CLI 或手动编辑的自定义字段 applyMessagingPlatformEntry(cfg, storageKey, normalizedAccountId, entry) @@ -5552,6 +5601,9 @@ const handlers = { if (platform === 'twitch') { return { valid: true, warnings: ['Twitch 面板已完成基础字段校验;实际连通性请通过 Gateway 启动日志或 openclaw channels status --probe 验证。'] } } + if (platform === 'nostr') { + return { valid: true, warnings: ['Nostr 面板已完成基础字段校验;实际连通性请通过 Gateway 启动日志或 openclaw channels status --probe 验证。'] } + } if (platform === 'discord') { try { const resp = await fetch('https://discord.com/api/v10/users/@me', { diff --git a/src-tauri/src/commands/messaging.rs b/src-tauri/src/commands/messaging.rs index 8bdccca..877e556 100644 --- a/src-tauri/src/commands/messaging.rs +++ b/src-tauri/src/commands/messaging.rs @@ -152,6 +152,7 @@ fn preserve_messaging_credential_refs( "gatewayPassword", "gatewayToken", "password", + "privateKey", "secretFile", "serviceAccount", "serviceAccountFile", @@ -239,6 +240,7 @@ fn channel_root_has_messaging_credential(root: &Map) -> bool { "gatewayPassword", "gatewayToken", "password", + "privateKey", "secretFile", "serviceAccount", "serviceAccountFile", @@ -276,6 +278,7 @@ fn required_channel_credential_fields( ("workspace", "Workspace"), ], "nextcloud-talk" => vec![("baseUrl", "Base URL")], + "nostr" => vec![("privateKey", "Private Key")], "twitch" => vec![ ("username", "Username"), ("accessToken", "Access Token"), @@ -964,6 +967,7 @@ fn normalize_messaging_platform_form( "remoteAttachmentRoots", "toolsAllow", "allowedRoles", + "relays", ] { if normalized.contains_key(key) { let items = json_array_from_csv_value(normalized.get(key)); @@ -1886,6 +1890,31 @@ pub async fn read_platform_config( insert_number_as_string(&mut form, &saved, "expiresIn"); insert_number_as_string(&mut form, &saved, "obtainmentTimestamp"); } + "nostr" => { + insert_secret_aware_form_value(&mut form, &saved, "privateKey"); + for key in ["name", "defaultAccount", "dmPolicy"] { + insert_string_if_present(&mut form, &saved, key); + } + insert_bool_as_string(&mut form, &saved, "enabled"); + insert_array_as_csv(&mut form, &saved, "relays"); + insert_array_as_csv(&mut form, &saved, "allowFrom"); + if let Some(profile) = saved.get("profile") { + for (source_key, form_key) in [ + ("name", "profileName"), + ("displayName", "profileDisplayName"), + ("about", "profileAbout"), + ("picture", "profilePicture"), + ("banner", "profileBanner"), + ("website", "profileWebsite"), + ("nip05", "profileNip05"), + ("lud16", "profileLud16"), + ] { + if let Some(v) = profile.get(source_key).and_then(|v| v.as_str()) { + form.insert(form_key.into(), Value::String(v.into())); + } + } + } + } "synology-chat" => { for key in ["token", "incomingUrl", "nasHost", "webhookPath", "botName"] { insert_secret_aware_form_value(&mut form, &saved, key); @@ -3083,6 +3112,47 @@ pub async fn save_messaging_platform( )?; ensure_plugin_allowed(&mut cfg, "twitch")?; } + "nostr" => { + let private_key = form_string(form_obj, "privateKey"); + if private_key.is_empty() && !has_configured_messaging_value(form_obj.get("privateKey")) + { + return Err("Nostr Private Key 不能为空".into()); + } + + let root_saved = channels_map + .get(storage_key.as_str()) + .cloned() + .unwrap_or(Value::Null); + 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", "defaultAccount", "privateKey", "dmPolicy"] { + put_string(&mut entry, key, form_string(form_obj, key)); + } + put_array_from_form_value(&mut entry, "relays", form_obj.get("relays")); + put_array_from_form_value(&mut entry, "allowFrom", form_obj.get("allowFrom")); + + let mut profile = Map::new(); + for (form_key, target_key) in [ + ("profileName", "name"), + ("profileDisplayName", "displayName"), + ("profileAbout", "about"), + ("profilePicture", "picture"), + ("profileBanner", "banner"), + ("profileWebsite", "website"), + ("profileNip05", "nip05"), + ("profileLud16", "lud16"), + ] { + put_string(&mut profile, target_key, form_string(form_obj, form_key)); + } + if !profile.is_empty() { + entry.insert("profile".into(), Value::Object(profile)); + } + + preserve_messaging_credential_refs(&mut entry, form_obj, &root_saved); + merge_channel_entry_for_account(channels_map, &storage_key, None, entry)?; + ensure_plugin_allowed(&mut cfg, "nostr")?; + } "synology-chat" => { let token = form_string(form_obj, "token"); let incoming_url = form_string(form_obj, "incomingUrl"); @@ -3392,6 +3462,10 @@ pub async fn verify_bot_token(platform: String, form: Value) -> Result Ok(json!({ + "valid": true, + "warnings": ["Nostr 面板已完成基础字段校验;实际连通性请通过 Gateway 启动日志或 openclaw channels status --probe 验证"] + })), _ => Ok(json!({ "valid": true, "warnings": ["该平台暂不支持在线校验"] @@ -6727,6 +6801,74 @@ mod tests { .contains("Access Token")); } + #[test] + fn normalize_nostr_form_preserves_relay_access_and_profile_fields() { + let form = json!({ + "enabled": "true", + "name": "nostr-bot", + "defaultAccount": "default", + "privateKey": "nsec1example", + "relays": "wss://relay.damus.io, wss://nos.lol", + "dmPolicy": "allowlist", + "allowFrom": "npub1sender, 0123456789abcdef", + "profileName": "openclaw", + "profileDisplayName": "OpenClaw Bot", + "profileAbout": "Nostr DM assistant", + "profilePicture": "https://example.com/avatar.png", + "profileWebsite": "https://example.com", + "profileNip05": "openclaw@example.com", + "profileLud16": "openclaw@example.com" + }); + let normalized = + normalize_messaging_platform_form("nostr", form.as_object().expect("object")); + + assert_eq!( + normalized.get("enabled").and_then(|v| v.as_bool()), + Some(true) + ); + assert_eq!( + normalized.get("dmPolicy").and_then(|v| v.as_str()), + Some("allowlist") + ); + assert_eq!( + normalized + .get("relays") + .and_then(|v| v.as_array()) + .map(|items| items.len()), + Some(2) + ); + assert_eq!( + normalized + .get("allowFrom") + .and_then(|v| v.as_array()) + .map(|items| items.len()), + Some(2) + ); + assert!(channel_diagnosis_credentials_ready("nostr", &normalized)); + + let missing = normalize_messaging_platform_form( + "nostr", + json!({ + "relays": "wss://relay.damus.io" + }) + .as_object() + .expect("object"), + ); + assert!(!channel_diagnosis_credentials_ready("nostr", &missing)); + let diagnosis = + build_openclaw_channel_diagnosis("nostr", 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("Private Key")); + } + #[test] fn channel_form_readback_preserves_mention_policy_choice() { let saved = json!({ diff --git a/src/locales/modules/channels.js b/src/locales/modules/channels.js index a32ed5b..25d875b 100644 --- a/src/locales/modules/channels.js +++ b/src/locales/modules/channels.js @@ -178,6 +178,18 @@ export default { twitchRefreshTokenHint: _('可选;与 Client Secret 配合用于刷新 Access Token。', 'Optional; used with Client Secret to refresh the access token.'), twitchExpiresInHint: _('Access Token 有效期,单位秒。', 'Access token lifetime in seconds.'), twitchObtainmentTimestampHint: _('Token 获取时间戳;上游用于判断刷新时机。', 'Token obtainment timestamp; upstream uses it to decide refresh timing.'), + nostrDesc: _('接入 Nostr 私信网络,支持 Relay 列表、私信访问策略和公开 Profile 配置', 'Connect Nostr direct messages with relay lists, DM access policy, and public profile settings'), + nostrGuide1: _('准备机器人账号的 Nostr 私钥,支持 nsec 或 64 位 hex 格式', 'Prepare the bot account Nostr private key, using nsec or 64-character hex format'), + nostrGuide2: _('填写 Gateway 可访问的 Relay URL,多个地址用逗号分隔', 'Fill relay URLs reachable by Gateway; separate multiple URLs with commas'), + nostrGuide3: _('按需设置 DM Policy 和 Allow From,限制允许发起私信的 npub 或公钥', 'Set DM Policy and Allow From as needed to limit which npub or public keys can start DMs'), + nostrGuide4: _('保存后面板会启用 Nostr 插件并重载 Gateway;连通性以 Gateway 日志或 channels status 为准', 'After saving, the panel enables the Nostr plugin and reloads Gateway; verify connectivity through Gateway logs or channels status'), + nostrGuideFooter: _('
Nostr 最小配置需要 Private Key;Relay 留空时上游会使用默认 Relay。
', '
Nostr minimally requires Private Key; upstream uses default relays when Relay URLs are empty.
'), + nostrPrivateKeyHint: _('生产环境建议使用 SecretRef;明文私钥会写入 openclaw.json。', 'Prefer SecretRef in production; plain-text private keys are written to openclaw.json.'), + nostrRelaysHint: _('可填写 ws:// 或 wss:// Relay URL,多个地址用逗号、分号或换行分隔。', 'Use ws:// or wss:// relay URLs; separate multiple values with commas, semicolons, or new lines.'), + nostrAllowFromHint: _('可选,逗号分隔允许发起私信的 npub 或 64 位公钥;dmPolicy=allowlist 时生效。', 'Optional comma-separated npub or 64-character public keys allowed to start DMs; used when dmPolicy=allowlist.'), + 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.'), synologyChatDesc: _('接入群晖 Synology Chat,适合 NAS 内网团队协作', 'Connect Synology Chat for NAS-hosted team messaging'), synologyChatGuide1: _('在 Synology Chat 管理后台创建 Bot,并复制 Token', 'Create a bot in Synology Chat administration and copy its Token'), synologyChatGuide2: _('配置 Incoming Webhook 或机器人发消息 URL,填入 Incoming URL', 'Configure an Incoming Webhook or bot post URL, then paste it as Incoming URL'), diff --git a/src/pages/channels.js b/src/pages/channels.js index 1a33814..ac432b9 100644 --- a/src/pages/channels.js +++ b/src/pages/channels.js @@ -386,6 +386,38 @@ const PLATFORM_REGISTRY = { pluginRequired: '@openclaw/twitch@latest', pluginId: 'twitch', }, + nostr: { + label: 'Nostr', + iconName: 'message-square', + desc: t('channels.nostrDesc'), + guide: [ + t('channels.nostrGuide1'), + t('channels.nostrGuide2'), + t('channels.nostrGuide3'), + t('channels.nostrGuide4'), + ], + guideFooter: t('channels.nostrGuideFooter'), + fields: [ + { key: 'privateKey', label: 'Private Key', placeholder: 'nsec1...', secret: true, required: true, hint: t('channels.nostrPrivateKeyHint') }, + { key: 'relays', label: 'Relay URLs', placeholder: 'wss://relay.damus.io, wss://nos.lol', required: false, hint: t('channels.nostrRelaysHint') }, + { key: 'dmPolicy', label: t('channels.dmPolicy'), type: 'select', options: DM_POLICY_OPTIONS, required: false }, + { key: 'allowFrom', label: 'Allow From', placeholder: 'npub1..., 0123456789abcdef', required: false, hint: t('channels.nostrAllowFromHint') }, + { key: 'name', label: t('channels.accountName'), placeholder: t('channels.optionalEg', { example: 'nostr-bot' }), required: false }, + { key: 'defaultAccount', label: 'Default Account', placeholder: 'default', required: false, hint: t('channels.nostrDefaultAccountHint') }, + { key: 'profileName', label: 'Profile Name', placeholder: 'openclaw', required: false }, + { key: 'profileDisplayName', label: 'Profile Display Name', placeholder: 'OpenClaw Bot', required: false }, + { key: 'profileAbout', label: 'Profile About', placeholder: t('channels.nostrProfileAboutPh'), multiline: true, required: false }, + { key: 'profilePicture', label: 'Profile Picture URL', placeholder: 'https://example.com/avatar.png', required: false, hint: t('channels.nostrProfileUrlHint') }, + { key: 'profileBanner', label: 'Profile Banner URL', placeholder: 'https://example.com/banner.png', required: false, hint: t('channels.nostrProfileUrlHint') }, + { key: 'profileWebsite', label: 'Profile Website', placeholder: 'https://example.com', required: false, hint: t('channels.nostrProfileUrlHint') }, + { key: 'profileNip05', label: 'NIP-05', placeholder: 'openclaw@example.com', required: false }, + { key: 'profileLud16', label: 'LUD-16', placeholder: 'openclaw@example.com', required: false }, + ], + configKey: 'nostr', + pairingChannel: 'nostr', + pluginRequired: '@openclaw/nostr@latest', + pluginId: 'nostr', + }, 'synology-chat': { label: 'Synology Chat', iconName: 'message-square', diff --git a/tests/channel-config-normalization.test.js b/tests/channel-config-normalization.test.js index 0745d20..f9c4bfc 100644 --- a/tests/channel-config-normalization.test.js +++ b/tests/channel-config-normalization.test.js @@ -439,6 +439,97 @@ test('Twitch 读取和诊断会回显访问控制与刷新 Token 字段', () => assert.equal(ready.checks.find(item => item.id === 'credentials')?.ok, true) }) +test('Nostr 渠道保存会写入上游根节点配置并启用插件', () => { + const cfg = { channels: {} } + + mergeOpenClawMessagingPlatformConfig(cfg, { + platform: 'nostr', + form: { + enabled: 'true', + name: 'nostr-bot', + defaultAccount: 'default', + privateKey: 'nsec1example', + relays: 'wss://relay.damus.io, wss://nos.lol', + dmPolicy: 'allowlist', + allowFrom: 'npub1sender, 0123456789abcdef', + profileName: 'openclaw', + profileDisplayName: 'OpenClaw Bot', + profileAbout: 'Nostr DM assistant', + profilePicture: 'https://example.com/avatar.png', + profileWebsite: 'https://example.com', + profileNip05: 'openclaw@example.com', + profileLud16: 'openclaw@example.com', + }, + }) + + const root = cfg.channels.nostr + assert.equal(root.enabled, true) + assert.equal(root.name, 'nostr-bot') + assert.equal(root.defaultAccount, 'default') + assert.equal(root.privateKey, 'nsec1example') + assert.deepEqual(root.relays, ['wss://relay.damus.io', 'wss://nos.lol']) + assert.equal(root.dmPolicy, 'allowlist') + assert.deepEqual(root.allowFrom, ['npub1sender', '0123456789abcdef']) + assert.equal(root.profile.name, 'openclaw') + assert.equal(root.profile.displayName, 'OpenClaw Bot') + assert.equal(root.profile.about, 'Nostr DM assistant') + assert.equal(root.profile.picture, 'https://example.com/avatar.png') + assert.equal(root.profile.website, 'https://example.com') + assert.equal(root.profile.nip05, 'openclaw@example.com') + assert.equal(root.profile.lud16, 'openclaw@example.com') + assert.equal(Object.hasOwn(root, 'accounts'), false) + assert.equal(cfg.plugins.entries.nostr.enabled, true) +}) + +test('Nostr 读取和诊断会回显 relay、访问控制和 profile 字段', () => { + const values = buildMessagingPlatformFormValues('nostr', { + enabled: true, + name: 'nostr-bot', + privateKey: 'nsec1example', + relays: ['wss://relay.damus.io', 'wss://nos.lol'], + dmPolicy: 'allowlist', + allowFrom: ['npub1sender'], + profile: { + name: 'openclaw', + displayName: 'OpenClaw Bot', + about: 'Nostr DM assistant', + picture: 'https://example.com/avatar.png', + website: 'https://example.com', + nip05: 'openclaw@example.com', + lud16: 'openclaw@example.com', + }, + }) + const missingKey = buildOpenClawChannelDiagnosis({ + platform: 'nostr', + configExists: true, + channelEnabled: true, + form: { relays: 'wss://relay.damus.io' }, + }) + const ready = buildOpenClawChannelDiagnosis({ + platform: 'nostr', + configExists: true, + channelEnabled: true, + form: values, + }) + + assert.equal(values.enabled, 'true') + assert.equal(values.name, 'nostr-bot') + assert.equal(values.privateKey, 'nsec1example') + assert.equal(values.relays, 'wss://relay.damus.io, wss://nos.lol') + assert.equal(values.dmPolicy, 'allowlist') + assert.equal(values.allowFrom, 'npub1sender') + assert.equal(values.profileName, 'openclaw') + assert.equal(values.profileDisplayName, 'OpenClaw Bot') + assert.equal(values.profileAbout, 'Nostr DM assistant') + assert.equal(values.profilePicture, 'https://example.com/avatar.png') + assert.equal(values.profileWebsite, 'https://example.com') + assert.equal(values.profileNip05, 'openclaw@example.com') + assert.equal(values.profileLud16, 'openclaw@example.com') + assert.equal(missingKey.checks.find(item => item.id === 'credentials')?.ok, false) + assert.match(missingKey.checks.find(item => item.id === 'credentials')?.detail || '', /Private Key/) + assert.equal(ready.checks.find(item => item.id === 'credentials')?.ok, true) +}) + test('Signal 渠道保存会保留多账号和上游运行字段', () => { const cfg = { channels: {} } diff --git a/tests/channel-ui-registry.test.js b/tests/channel-ui-registry.test.js index b991bde..60f7d80 100644 --- a/tests/channel-ui-registry.test.js +++ b/tests/channel-ui-registry.test.js @@ -139,3 +139,25 @@ test('Twitch 渠道 UI 会暴露聊天账号和访问控制配置字段', () => assert.match(twitchBlock, /pluginRequired:\s*'@openclaw\/twitch@latest'/) assert.match(twitchBlock, /pluginId:\s*'twitch'/) }) + +test('Nostr 渠道 UI 会暴露私钥、Relay、访问控制和 Profile 配置字段', () => { + const nostrBlock = getRegistryBlock('nostr') + + for (const field of [ + 'privateKey', + 'relays', + 'dmPolicy', + 'allowFrom', + 'profileName', + 'profileDisplayName', + 'profileAbout', + 'profilePicture', + 'profileWebsite', + 'profileNip05', + 'profileLud16', + ]) { + assert.match(nostrBlock, new RegExp(`key:\\s*'${field}'`)) + } + assert.match(nostrBlock, /pluginRequired:\s*'@openclaw\/nostr@latest'/) + assert.match(nostrBlock, /pluginId:\s*'nostr'/) +})