diff --git a/scripts/dev-api.js b/scripts/dev-api.js index d84fad6..4a7f77b 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -3675,6 +3675,30 @@ export function mergeHermesQuickCommandsConfig(config = {}, form = {}) { return next } +function normalizeHermesUnauthorizedDmBehavior(value, strict = false) { + const normalized = String(value ?? '').trim().toLowerCase() + if (['pair', 'ignore'].includes(normalized)) return normalized + if (strict) throw new Error('unauthorized_dm_behavior 必须是 pair 或 ignore') + return 'pair' +} + +export function buildHermesUnauthorizedDmConfigValues(config = {}) { + const root = config && typeof config === 'object' && !Array.isArray(config) ? config : {} + return { + unauthorizedDmBehavior: normalizeHermesUnauthorizedDmBehavior(root.unauthorized_dm_behavior, false), + } +} + +export function mergeHermesUnauthorizedDmConfig(config = {}, form = {}) { + const next = mergeConfigsPreservingFields({}, config && typeof config === 'object' && !Array.isArray(config) ? config : {}) + const currentValues = buildHermesUnauthorizedDmConfigValues(next) + next.unauthorized_dm_behavior = normalizeHermesUnauthorizedDmBehavior( + Object.hasOwn(form, 'unauthorizedDmBehavior') ? form.unauthorizedDmBehavior : currentValues.unauthorizedDmBehavior, + true, + ) + return next +} + export function buildHermesStreamingConfigValues(config = {}) { const root = config && typeof config === 'object' && !Array.isArray(config) ? config : {} const streaming = hermesStreamingConfigSource(root) @@ -10181,6 +10205,27 @@ const handlers = { } }, + hermes_unauthorized_dm_config_read() { + const { configPath, exists, config } = readHermesConfigYamlObject() + return { + exists, + configPath, + values: buildHermesUnauthorizedDmConfigValues(config), + } + }, + + hermes_unauthorized_dm_config_save({ form } = {}) { + const { configPath, config } = readHermesConfigYamlObject() + const next = mergeHermesUnauthorizedDmConfig(config, form || {}) + const backup = writeHermesConfigYamlObject(configPath, next) + return { + ok: true, + configPath, + backup, + values: buildHermesUnauthorizedDmConfigValues(next), + } + }, + hermes_streaming_config_read() { const { configPath, exists, config } = readHermesConfigYamlObject() return { diff --git a/src-tauri/src/commands/hermes.rs b/src-tauri/src/commands/hermes.rs index 56f9c47..b01a3e8 100644 --- a/src-tauri/src/commands/hermes.rs +++ b/src-tauri/src/commands/hermes.rs @@ -3856,6 +3856,55 @@ fn merge_hermes_quick_commands_config( Ok(()) } +fn normalize_hermes_unauthorized_dm_behavior( + value: Option, + strict: bool, +) -> Result { + let behavior = value.unwrap_or_default().trim().to_ascii_lowercase(); + if matches!(behavior.as_str(), "pair" | "ignore") { + return Ok(behavior); + } + if strict { + Err("unauthorized_dm_behavior 必须是 pair 或 ignore".to_string()) + } else { + Ok("pair".to_string()) + } +} + +fn build_hermes_unauthorized_dm_config_values(config: &serde_yaml::Value) -> Value { + let root = config.as_mapping(); + let behavior = root + .and_then(|map| yaml_string_field(map, "unauthorized_dm_behavior")) + .and_then(|value| normalize_hermes_unauthorized_dm_behavior(Some(value), false).ok()) + .unwrap_or_else(|| "pair".to_string()); + + serde_json::json!({ + "unauthorizedDmBehavior": behavior, + }) +} + +fn merge_hermes_unauthorized_dm_config( + config: &mut serde_yaml::Value, + form: &Value, +) -> Result<(), String> { + let current = build_hermes_unauthorized_dm_config_values(config); + let behavior = normalize_hermes_unauthorized_dm_behavior( + form_string(form, "unauthorizedDmBehavior").or_else(|| { + current["unauthorizedDmBehavior"] + .as_str() + .map(ToString::to_string) + }), + true, + )?; + + let root = ensure_yaml_object(config)?; + root.insert( + yaml_key("unauthorized_dm_behavior"), + serde_yaml::Value::String(behavior), + ); + Ok(()) +} + fn normalize_hermes_streaming_transport( value: Option, strict: bool, @@ -5374,6 +5423,30 @@ pub fn hermes_quick_commands_config_save(form: Value) -> Result { })) } +#[tauri::command] +pub fn hermes_unauthorized_dm_config_read() -> Result { + let (config_path, exists, config) = read_hermes_channel_yaml_config()?; + ensure_yaml_object(&mut config.clone())?; + Ok(serde_json::json!({ + "exists": exists, + "configPath": config_path.to_string_lossy(), + "values": build_hermes_unauthorized_dm_config_values(&config), + })) +} + +#[tauri::command] +pub fn hermes_unauthorized_dm_config_save(form: Value) -> Result { + let (config_path, _exists, mut config) = read_hermes_channel_yaml_config()?; + merge_hermes_unauthorized_dm_config(&mut config, &form)?; + let backup = write_hermes_yaml_config(&config_path, &config)?; + Ok(serde_json::json!({ + "ok": true, + "configPath": config_path.to_string_lossy(), + "backup": backup, + "values": build_hermes_unauthorized_dm_config_values(&config), + })) +} + #[tauri::command] pub fn hermes_streaming_config_read() -> Result { let (config_path, exists, config) = read_hermes_channel_yaml_config()?; @@ -11398,6 +11471,75 @@ streaming: } } +#[cfg(test)] +mod hermes_unauthorized_dm_config_tests { + use super::{build_hermes_unauthorized_dm_config_values, merge_hermes_unauthorized_dm_config}; + use serde_json::json; + + #[test] + fn unauthorized_dm_values_have_pair_default() { + let config: serde_yaml::Value = serde_yaml::from_str("{}").unwrap(); + let values = build_hermes_unauthorized_dm_config_values(&config); + assert_eq!(values["unauthorizedDmBehavior"], "pair"); + } + + #[test] + fn unauthorized_dm_values_normalize_existing_behavior() { + let config: serde_yaml::Value = + serde_yaml::from_str("unauthorized_dm_behavior: IGNORE").unwrap(); + let values = build_hermes_unauthorized_dm_config_values(&config); + assert_eq!(values["unauthorizedDmBehavior"], "ignore"); + + let config: serde_yaml::Value = + serde_yaml::from_str("unauthorized_dm_behavior: silent").unwrap(); + let values = build_hermes_unauthorized_dm_config_values(&config); + assert_eq!(values["unauthorizedDmBehavior"], "pair"); + } + + #[test] + fn merge_unauthorized_dm_config_preserves_unrelated_yaml() { + let mut config: serde_yaml::Value = serde_yaml::from_str( + r#" +model: + provider: anthropic +unauthorized_dm_behavior: pair +platforms: + telegram: + enabled: true + custom_flag: keep-platform +memory: + memory_enabled: true +"#, + ) + .unwrap(); + + merge_hermes_unauthorized_dm_config( + &mut config, + &json!({ "unauthorizedDmBehavior": "ignore" }), + ) + .unwrap(); + + assert_eq!(config["model"]["provider"].as_str(), Some("anthropic")); + assert_eq!(config["memory"]["memory_enabled"].as_bool(), Some(true)); + assert_eq!( + config["platforms"]["telegram"]["custom_flag"].as_str(), + Some("keep-platform") + ); + assert_eq!(config["unauthorized_dm_behavior"].as_str(), Some("ignore")); + } + + #[test] + fn merge_unauthorized_dm_config_rejects_invalid_values() { + let mut config = serde_yaml::Value::Mapping(serde_yaml::Mapping::new()); + let err = merge_hermes_unauthorized_dm_config( + &mut config, + &json!({ "unauthorizedDmBehavior": "silent" }), + ) + .unwrap_err(); + assert!(err.contains("unauthorized_dm_behavior")); + } +} + #[cfg(test)] mod hermes_channel_tests { use super::{ diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6f26636..f181f7c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -269,6 +269,8 @@ pub fn run() { hermes::hermes_skills_config_save, hermes::hermes_quick_commands_config_read, hermes::hermes_quick_commands_config_save, + hermes::hermes_unauthorized_dm_config_read, + hermes::hermes_unauthorized_dm_config_save, hermes::hermes_streaming_config_read, hermes::hermes_streaming_config_save, hermes::hermes_execution_limits_config_read, diff --git a/src/engines/hermes/pages/config.js b/src/engines/hermes/pages/config.js index ffbd715..ddcc401 100644 --- a/src/engines/hermes/pages/config.js +++ b/src/engines/hermes/pages/config.js @@ -52,6 +52,10 @@ const QUICK_COMMANDS_DEFAULTS = { quickCommandsJson: '{}', } +const UNAUTHORIZED_DM_DEFAULTS = { + unauthorizedDmBehavior: 'pair', +} + const STREAMING_DEFAULTS = { enabled: false, transport: 'edit', @@ -91,6 +95,7 @@ const SESSION_RESET_MODES = ['both', 'idle', 'daily', 'none'] const STREAMING_TRANSPORTS = ['edit', 'auto', 'draft', 'off'] const CODE_EXECUTION_MODES = ['project', 'strict'] const TERMINAL_BACKENDS = ['local', 'ssh', 'docker', 'singularity', 'modal', 'daytona', 'vercel_sandbox'] +const UNAUTHORIZED_DM_BEHAVIORS = ['pair', 'ignore'] export function render() { const el = document.createElement('div') @@ -103,6 +108,7 @@ export function render() { let memoryValues = { ...MEMORY_DEFAULTS } let skillsValues = { ...SKILLS_DEFAULTS } let quickCommandsValues = { ...QUICK_COMMANDS_DEFAULTS } + let unauthorizedDmValues = { ...UNAUTHORIZED_DM_DEFAULTS } let streamingValues = { ...STREAMING_DEFAULTS } let executionLimitsValues = { ...EXECUTION_LIMITS_DEFAULTS } let terminalValues = { ...TERMINAL_DEFAULTS } @@ -113,6 +119,7 @@ export function render() { let memoryLoading = true let skillsLoading = true let quickCommandsLoading = true + let unauthorizedDmLoading = true let streamingLoading = true let executionLimitsLoading = true let terminalLoading = true @@ -123,6 +130,7 @@ export function render() { let memorySaving = false let skillsSaving = false let quickCommandsSaving = false + let unauthorizedDmSaving = false let streamingSaving = false let executionLimitsSaving = false let terminalSaving = false @@ -133,6 +141,7 @@ export function render() { let memoryError = null let skillsError = null let quickCommandsError = null + let unauthorizedDmError = null let streamingError = null let executionLimitsError = null let terminalError = null @@ -146,7 +155,7 @@ export function render() { } function isBusy() { - return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || streamingLoading || executionLimitsLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || streamingSaving || executionLimitsSaving || terminalSaving + return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || unauthorizedDmLoading || streamingLoading || executionLimitsLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving } function option(labelKey, value, selected) { @@ -163,7 +172,7 @@ export function render() { } function renderRuntimePanel() { - const disabled = loading || saving || runtimeLoading || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || streamingSaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || runtimeLoading || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving return `
@@ -211,7 +220,7 @@ export function render() { } function renderCompressionPanel() { - const disabled = loading || saving || compressionLoading || compressionSaving || runtimeSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || streamingSaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || compressionLoading || compressionSaving || runtimeSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving return `
@@ -261,7 +270,7 @@ export function render() { } function renderToolGuardrailsPanel() { - const disabled = loading || saving || toolGuardrailsLoading || toolGuardrailsSaving || runtimeSaving || compressionSaving || memorySaving || skillsSaving || quickCommandsSaving || streamingSaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || toolGuardrailsLoading || toolGuardrailsSaving || runtimeSaving || compressionSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving return `
@@ -430,6 +439,36 @@ export function render() { ` } + function renderUnauthorizedDmConfigPanel() { + const disabled = loading || saving || unauthorizedDmLoading || unauthorizedDmSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || streamingSaving || executionLimitsSaving || terminalSaving + return ` +
+
+
+
${t('engine.hermesUnauthorizedDmConfigTitle')}
+
${t('engine.hermesUnauthorizedDmConfigDesc')}
+
+
+ ${unauthorizedDmSaving ? t('engine.hermesConfigStatusSaving') : unauthorizedDmLoading ? t('engine.hermesConfigStatusLoading') : t('engine.hermesUnauthorizedDmConfigStatusReady')} + +
+
+
+ ${renderError(unauthorizedDmError)} +
+ +
+
${t('engine.hermesUnauthorizedDmConfigFootnote')}
+
+
+ ` + } + function renderStreamingPanel() { const disabled = loading || saving || streamingLoading || streamingSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || executionLimitsSaving || terminalSaving return ` @@ -483,7 +522,7 @@ export function render() { } function renderExecutionLimitsPanel() { - const disabled = loading || saving || executionLimitsLoading || executionLimitsSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || streamingSaving + const disabled = loading || saving || executionLimitsLoading || executionLimitsSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving return `
@@ -555,7 +594,7 @@ export function render() { } function renderTerminalPanel() { - const disabled = loading || saving || terminalLoading || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || streamingSaving || executionLimitsSaving + const disabled = loading || saving || terminalLoading || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving return `
@@ -648,6 +687,7 @@ export function render() { ${renderMemoryPanel()} ${renderSkillsConfigPanel()} ${renderQuickCommandsConfigPanel()} + ${renderUnauthorizedDmConfigPanel()}
@@ -673,6 +713,7 @@ export function render() { el.querySelector('#hm-memory-save')?.addEventListener('click', saveMemory) el.querySelector('#hm-skills-config-save')?.addEventListener('click', saveSkillsConfig) el.querySelector('#hm-quick-commands-save')?.addEventListener('click', saveQuickCommandsConfig) + el.querySelector('#hm-unauthorized-dm-save')?.addEventListener('click', saveUnauthorizedDmConfig) el.querySelector('#hm-streaming-save')?.addEventListener('click', saveStreaming) el.querySelector('#hm-execution-limits-save')?.addEventListener('click', saveExecutionLimits) el.querySelector('#hm-terminal-save')?.addEventListener('click', saveTerminal) @@ -713,6 +754,11 @@ export function render() { quickCommandsValues = { ...QUICK_COMMANDS_DEFAULTS, ...(data?.values || {}) } } + async function loadUnauthorizedDmConfig() { + const data = await api.hermesUnauthorizedDmConfigRead() + unauthorizedDmValues = { ...UNAUTHORIZED_DM_DEFAULTS, ...(data?.values || {}) } + } + async function loadStreaming() { const data = await api.hermesStreamingConfigRead() streamingValues = { ...STREAMING_DEFAULTS, ...(data?.values || {}) } @@ -736,6 +782,7 @@ export function render() { memoryLoading = true skillsLoading = true quickCommandsLoading = true + unauthorizedDmLoading = true streamingLoading = true executionLimitsLoading = true terminalLoading = true @@ -746,6 +793,7 @@ export function render() { memoryError = null skillsError = null quickCommandsError = null + unauthorizedDmError = null streamingError = null executionLimitsError = null terminalError = null @@ -829,6 +877,14 @@ export function render() { quickCommandsLoading = false draw() } + try { + await loadUnauthorizedDmConfig() + } catch (err) { + unauthorizedDmError = humanizeError(err, t('engine.hermesUnauthorizedDmConfigLoadFailed') || 'Load unauthorized DM config failed') + } finally { + unauthorizedDmLoading = false + draw() + } } async function refreshRawAfterStructuredSave() { @@ -868,6 +924,9 @@ export function render() { try { await loadQuickCommandsConfig() } catch {} + try { + await loadUnauthorizedDmConfig() + } catch {} try { await loadStreaming() } catch {} @@ -1058,6 +1117,31 @@ export function render() { } } + async function saveUnauthorizedDmConfig() { + const form = { + unauthorizedDmBehavior: el.querySelector('#hm-unauthorized-dm-behavior')?.value || 'pair', + } + unauthorizedDmSaving = true + unauthorizedDmError = null + draw() + try { + const result = await api.hermesUnauthorizedDmConfigSave(form) + unauthorizedDmValues = { ...UNAUTHORIZED_DM_DEFAULTS, ...(result?.values || form) } + await refreshRawAfterStructuredSave() + const backup = result?.backup || '' + toast({ + message: t('engine.hermesUnauthorizedDmConfigSaveSuccess'), + hint: backup ? t('engine.hermesConfigBackupHint', { path: backup }) : '', + }, 'success') + } catch (err) { + unauthorizedDmError = humanizeError(err, t('engine.hermesUnauthorizedDmConfigSaveFailed') || 'Save unauthorized DM config failed') + toast(unauthorizedDmError, 'error') + } finally { + unauthorizedDmSaving = false + draw() + } + } + async function saveStreaming() { const form = { enabled: !!el.querySelector('#hm-streaming-enabled')?.checked, diff --git a/src/lib/tauri-api.js b/src/lib/tauri-api.js index 7d7432a..56321bc 100644 --- a/src/lib/tauri-api.js +++ b/src/lib/tauri-api.js @@ -521,6 +521,8 @@ export const api = { hermesSkillsConfigSave: (form) => invoke('hermes_skills_config_save', { form }), hermesQuickCommandsConfigRead: () => invoke('hermes_quick_commands_config_read'), hermesQuickCommandsConfigSave: (form) => invoke('hermes_quick_commands_config_save', { form }), + hermesUnauthorizedDmConfigRead: () => invoke('hermes_unauthorized_dm_config_read'), + hermesUnauthorizedDmConfigSave: (form) => invoke('hermes_unauthorized_dm_config_save', { form }), hermesStreamingConfigRead: () => invoke('hermes_streaming_config_read'), hermesStreamingConfigSave: (form) => invoke('hermes_streaming_config_save', { form }), hermesExecutionLimitsConfigRead: () => invoke('hermes_execution_limits_config_read'), diff --git a/src/locales/modules/engine.js b/src/locales/modules/engine.js index 5c994c5..df1ec83 100644 --- a/src/locales/modules/engine.js +++ b/src/locales/modules/engine.js @@ -629,6 +629,17 @@ export default { hermesQuickCommandsConfigSaveFailed: _('保存快捷命令失败', 'Save quick commands failed', '儲存快捷命令失敗'), hermesQuickCommandsConfigJson: _('quick_commands JSON 映射', 'quick_commands JSON map', 'quick_commands JSON 映射'), hermesQuickCommandsConfigFootnote: _('键名会变成斜杠命令,例如 status 对应 /status。每个命令必须是对象,type 只能为 exec 或 alias;exec 需要 command,alias 的 target 必须以 / 开头。', 'Keys become slash commands, for example status maps to /status. Each command must be an object with type exec or alias; exec needs command, and alias target must start with /.', '鍵名會變成斜線命令,例如 status 對應 /status。每個命令必須是物件,type 只能是 exec 或 alias;exec 需要 command,alias 的 target 必須以 / 開頭。'), + hermesUnauthorizedDmConfigTitle: _('未授权私信', 'Unauthorized DMs', '未授權私訊'), + hermesUnauthorizedDmConfigDesc: _('控制陌生用户直接私信 Hermes 时的全局响应策略,适合公网部署时减少无效打扰或保留配对入口。', 'Control the global response when unknown users send Hermes a direct message. Useful for public deployments that need fewer unsolicited replies or a pairing entry point.', '控制陌生使用者直接私訊 Hermes 時的全域回應策略,適合公開部署時減少無效打擾或保留配對入口。'), + hermesUnauthorizedDmConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'), + hermesUnauthorizedDmConfigSave: _('保存私信策略', 'Save DM policy', '儲存私訊策略'), + hermesUnauthorizedDmConfigSaveSuccess: _('未授权私信策略已保存,建议重启 Hermes Gateway 生效', 'Unauthorized DM policy saved. Restart Hermes Gateway to take effect.', '未授權私訊策略已儲存,建議重啟 Hermes Gateway 生效'), + hermesUnauthorizedDmConfigLoadFailed: _('加载未授权私信策略失败', 'Load unauthorized DM policy failed', '載入未授權私訊策略失敗'), + hermesUnauthorizedDmConfigSaveFailed: _('保存未授权私信策略失败', 'Save unauthorized DM policy failed', '儲存未授權私訊策略失敗'), + hermesUnauthorizedDmConfigBehavior: _('陌生私信处理方式', 'Unknown DM handling', '陌生私訊處理方式'), + hermesUnauthorizedDmConfigBehavior_pair: _('回复配对码', 'Reply with pairing code', '回覆配對碼'), + hermesUnauthorizedDmConfigBehavior_ignore: _('静默忽略', 'Silently ignore', '靜默忽略'), + hermesUnauthorizedDmConfigFootnote: _('pair 是默认值,会拒绝访问但在私信中回复一次性配对码;ignore 会静默丢弃陌生私信。平台级覆盖仍可在渠道配置或 raw YAML 中单独设置。', 'pair is the default: Hermes denies access but replies with a one-time pairing code in DMs. ignore silently drops unknown DMs. Platform-level overrides can still be set in channel settings or raw YAML.', 'pair 是預設值,會拒絕存取但在私訊中回覆一次性配對碼;ignore 會靜默丟棄陌生私訊。平台級覆蓋仍可在頻道設定或 raw YAML 中單獨設定。'), // Batch 1 §E: 会话导出 sessionsExport: _('导出', 'Export', '匯出'), sessionsExportSuccess: _('已导出', 'Exported', '已匯出'), diff --git a/tests/hermes-config-page-ui.test.js b/tests/hermes-config-page-ui.test.js index aa046eb..f99597a 100644 --- a/tests/hermes-config-page-ui.test.js +++ b/tests/hermes-config-page-ui.test.js @@ -58,6 +58,15 @@ test('Hermes 配置页会暴露快捷命令结构化配置字段', () => { } }) +test('Hermes 配置页会暴露未授权 DM 全局策略字段', () => { + for (const id of [ + 'hm-unauthorized-dm-save', + 'hm-unauthorized-dm-behavior', + ]) { + assert.match(source, new RegExp(`id="${id}"`), `缺少 ${id}`) + } +}) + test('Hermes 配置页会暴露网关流式结构化配置字段', () => { for (const id of [ 'hm-streaming-save', @@ -118,6 +127,7 @@ test('Hermes 配置页新增结构化配置不会暴露翻译 key', () => { key.includes('MemoryConfig') || key.includes('SkillsConfig') || key.includes('QuickCommandsConfig') || + key.includes('UnauthorizedDmConfig') || key.includes('StreamingConfig') || key.includes('ExecutionLimits') || key.includes('TerminalConfig') diff --git a/tests/hermes-unauthorized-dm-config.test.js b/tests/hermes-unauthorized-dm-config.test.js new file mode 100644 index 0000000..cba3078 --- /dev/null +++ b/tests/hermes-unauthorized-dm-config.test.js @@ -0,0 +1,45 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { + buildHermesUnauthorizedDmConfigValues, + mergeHermesUnauthorizedDmConfig, +} from '../scripts/dev-api.js' + +test('Hermes 未授权 DM 配置读取会提供默认配对策略', () => { + const values = buildHermesUnauthorizedDmConfigValues({}) + + assert.deepEqual(values, { + unauthorizedDmBehavior: 'pair', + }) +}) + +test('Hermes 未授权 DM 配置读取会规范化已有策略', () => { + assert.equal(buildHermesUnauthorizedDmConfigValues({ unauthorized_dm_behavior: 'IGNORE' }).unauthorizedDmBehavior, 'ignore') + assert.equal(buildHermesUnauthorizedDmConfigValues({ unauthorized_dm_behavior: 'bad' }).unauthorizedDmBehavior, 'pair') +}) + +test('Hermes 未授权 DM 配置保存会保留无关 YAML 并写入顶层策略', () => { + const next = mergeHermesUnauthorizedDmConfig({ + model: { provider: 'anthropic' }, + unauthorized_dm_behavior: 'pair', + platforms: { + telegram: { enabled: true, custom_flag: 'keep-platform' }, + }, + memory: { memory_enabled: true }, + }, { + unauthorizedDmBehavior: 'ignore', + }) + + assert.deepEqual(next.model, { provider: 'anthropic' }) + assert.deepEqual(next.memory, { memory_enabled: true }) + assert.deepEqual(next.platforms.telegram, { enabled: true, custom_flag: 'keep-platform' }) + assert.equal(next.unauthorized_dm_behavior, 'ignore') +}) + +test('Hermes 未授权 DM 配置保存会拒绝非法策略', () => { + assert.throws( + () => mergeHermesUnauthorizedDmConfig({}, { unauthorizedDmBehavior: 'silent' }), + /unauthorized_dm_behavior/, + ) +})