mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 04:10:00 +08:00
feat(channels): add Nostr config compatibility
This commit is contained in:
@@ -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', {
|
||||
|
||||
@@ -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<String, Value>) -> 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<Value, St
|
||||
"valid": true,
|
||||
"warnings": ["Twitch 面板已完成基础字段校验;实际连通性请通过 Gateway 启动日志或 openclaw channels status --probe 验证"]
|
||||
})),
|
||||
"nostr" => 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!({
|
||||
|
||||
@@ -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: _('<div style="margin-top:8px;font-size:var(--font-size-xs);color:var(--text-tertiary)">Nostr 最小配置需要 Private Key;Relay 留空时上游会使用默认 Relay。</div>', '<div style="margin-top:8px;font-size:var(--font-size-xs);color:var(--text-tertiary)">Nostr minimally requires Private Key; upstream uses default relays when Relay URLs are empty.</div>'),
|
||||
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'),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: {} }
|
||||
|
||||
|
||||
@@ -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'/)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user