mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 04:10:00 +08:00
feat(hermes): add display reliability settings
This commit is contained in:
@@ -3326,6 +3326,9 @@ const HERMES_CODE_EXECUTION_MODES = new Set(['project', 'strict'])
|
||||
const HERMES_TERMINAL_BACKENDS = new Set(['local', 'ssh', 'docker', 'singularity', 'modal', 'daytona', 'vercel_sandbox'])
|
||||
const HERMES_DISPLAY_TOOL_PROGRESS_VALUES = new Set(['off', 'new', 'all', 'verbose'])
|
||||
const HERMES_DISPLAY_STREAMING_VALUES = new Set(['inherit', 'true', 'false'])
|
||||
const HERMES_DISPLAY_RESUME_VALUES = new Set(['full', 'minimal'])
|
||||
const HERMES_DISPLAY_LANGUAGE_VALUES = new Set(['en', 'zh', 'zh-hant', 'ja', 'de', 'es', 'fr', 'tr', 'uk', 'af', 'ko', 'it', 'ga', 'pt', 'ru', 'hu'])
|
||||
const HERMES_RUNTIME_FOOTER_FIELDS = new Set(['model', 'context_pct', 'cwd', 'duration', 'tokens', 'cost'])
|
||||
|
||||
function parseHermesInteger(value, key, fallback, min, max, strict = false) {
|
||||
const raw = String(value ?? '').trim()
|
||||
@@ -3413,6 +3416,34 @@ function normalizeHermesDisplayStreaming(value, strict = false, key = 'display.s
|
||||
return 'inherit'
|
||||
}
|
||||
|
||||
function normalizeHermesDisplayResume(value, strict = false) {
|
||||
const mode = String(value ?? '').trim().toLowerCase() || 'full'
|
||||
if (HERMES_DISPLAY_RESUME_VALUES.has(mode)) return mode
|
||||
if (strict) throw new Error('display.resume_display 必须是 full 或 minimal')
|
||||
return 'full'
|
||||
}
|
||||
|
||||
function normalizeHermesDisplayLanguage(value, strict = false) {
|
||||
const language = String(value ?? '').trim().toLowerCase() || 'en'
|
||||
if (HERMES_DISPLAY_LANGUAGE_VALUES.has(language)) return language
|
||||
if (strict) throw new Error('display.language 不在支持列表中')
|
||||
return 'en'
|
||||
}
|
||||
|
||||
function normalizeHermesRuntimeFooterFields(value, strict = false) {
|
||||
const items = Array.isArray(value)
|
||||
? value
|
||||
: String(value ?? '').split(/\r?\n|,/)
|
||||
const normalized = [...new Set(items.map(item => String(item ?? '').trim()).filter(Boolean))]
|
||||
if (!normalized.length) return ['model', 'context_pct', 'cwd']
|
||||
const invalid = normalized.find(item => !HERMES_RUNTIME_FOOTER_FIELDS.has(item))
|
||||
if (invalid) {
|
||||
if (strict) throw new Error(`display.runtime_footer.fields 包含不支持的字段: ${invalid}`)
|
||||
return ['model', 'context_pct', 'cwd']
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
function hermesDisplayConfigParts(config = {}, platform = '') {
|
||||
const display = config?.display && typeof config.display === 'object' && !Array.isArray(config.display)
|
||||
? config.display
|
||||
@@ -3426,6 +3457,49 @@ function hermesDisplayConfigParts(config = {}, platform = '') {
|
||||
return { display, platformDisplay }
|
||||
}
|
||||
|
||||
export function buildHermesDisplayConfigValues(config = {}) {
|
||||
const root = config && typeof config === 'object' && !Array.isArray(config) ? config : {}
|
||||
const display = root.display && typeof root.display === 'object' && !Array.isArray(root.display)
|
||||
? root.display
|
||||
: {}
|
||||
const runtimeFooter = display.runtime_footer && typeof display.runtime_footer === 'object' && !Array.isArray(display.runtime_footer)
|
||||
? display.runtime_footer
|
||||
: {}
|
||||
return {
|
||||
displayToolProgress: normalizeHermesDisplayToolProgress(display.tool_progress, false),
|
||||
displayToolProgressCommand: readHermesBool(display.tool_progress_command, false),
|
||||
displayInterimAssistantMessages: readHermesBool(display.interim_assistant_messages, true),
|
||||
displayRuntimeFooterEnabled: readHermesBool(runtimeFooter.enabled, false),
|
||||
displayRuntimeFooterFields: normalizeHermesRuntimeFooterFields(runtimeFooter.fields, false).join('\n'),
|
||||
displayFileMutationVerifier: readHermesBool(display.file_mutation_verifier, true),
|
||||
displayLanguage: normalizeHermesDisplayLanguage(display.language, false),
|
||||
displayResumeDisplay: normalizeHermesDisplayResume(display.resume_display, false),
|
||||
}
|
||||
}
|
||||
|
||||
export function mergeHermesDisplayConfig(config = {}, form = {}) {
|
||||
const next = mergeConfigsPreservingFields({}, config && typeof config === 'object' && !Array.isArray(config) ? config : {})
|
||||
const currentValues = buildHermesDisplayConfigValues(next)
|
||||
const display = next.display && typeof next.display === 'object' && !Array.isArray(next.display)
|
||||
? mergeConfigsPreservingFields(next.display, {})
|
||||
: {}
|
||||
const runtimeFooter = display.runtime_footer && typeof display.runtime_footer === 'object' && !Array.isArray(display.runtime_footer)
|
||||
? mergeConfigsPreservingFields(display.runtime_footer, {})
|
||||
: {}
|
||||
|
||||
display.tool_progress = normalizeHermesDisplayToolProgress(Object.hasOwn(form, 'displayToolProgress') ? form.displayToolProgress : currentValues.displayToolProgress, true, 'display.tool_progress')
|
||||
display.tool_progress_command = formHermesBool(form, 'displayToolProgressCommand', currentValues.displayToolProgressCommand)
|
||||
display.interim_assistant_messages = formHermesBool(form, 'displayInterimAssistantMessages', currentValues.displayInterimAssistantMessages)
|
||||
runtimeFooter.enabled = formHermesBool(form, 'displayRuntimeFooterEnabled', currentValues.displayRuntimeFooterEnabled)
|
||||
runtimeFooter.fields = normalizeHermesRuntimeFooterFields(Object.hasOwn(form, 'displayRuntimeFooterFields') ? form.displayRuntimeFooterFields : currentValues.displayRuntimeFooterFields, true)
|
||||
display.runtime_footer = runtimeFooter
|
||||
display.file_mutation_verifier = formHermesBool(form, 'displayFileMutationVerifier', currentValues.displayFileMutationVerifier)
|
||||
display.language = normalizeHermesDisplayLanguage(Object.hasOwn(form, 'displayLanguage') ? form.displayLanguage : currentValues.displayLanguage, true)
|
||||
display.resume_display = normalizeHermesDisplayResume(Object.hasOwn(form, 'displayResumeDisplay') ? form.displayResumeDisplay : currentValues.displayResumeDisplay, true)
|
||||
next.display = display
|
||||
return next
|
||||
}
|
||||
|
||||
function putHermesChannelDisplayFields(form, config, platform) {
|
||||
const { display, platformDisplay } = hermesDisplayConfigParts(config, platform)
|
||||
const legacyToolProgress = display.tool_progress_overrides && typeof display.tool_progress_overrides === 'object' && !Array.isArray(display.tool_progress_overrides)
|
||||
@@ -10338,6 +10412,27 @@ const handlers = {
|
||||
}
|
||||
},
|
||||
|
||||
hermes_display_config_read() {
|
||||
const { configPath, exists, config } = readHermesConfigYamlObject()
|
||||
return {
|
||||
exists,
|
||||
configPath,
|
||||
values: buildHermesDisplayConfigValues(config),
|
||||
}
|
||||
},
|
||||
|
||||
hermes_display_config_save({ form } = {}) {
|
||||
const { configPath, config } = readHermesConfigYamlObject()
|
||||
const next = mergeHermesDisplayConfig(config, form || {})
|
||||
const backup = writeHermesConfigYamlObject(configPath, next)
|
||||
return {
|
||||
ok: true,
|
||||
configPath,
|
||||
backup,
|
||||
values: buildHermesDisplayConfigValues(next),
|
||||
}
|
||||
},
|
||||
|
||||
hermes_streaming_config_read() {
|
||||
const { configPath, exists, config } = readHermesConfigYamlObject()
|
||||
return {
|
||||
|
||||
@@ -4004,6 +4004,263 @@ fn normalize_hermes_human_delay_mode(
|
||||
}
|
||||
}
|
||||
|
||||
const HERMES_DISPLAY_LANGUAGE_VALUES: &[&str] = &[
|
||||
"en", "zh", "zh-hant", "ja", "de", "es", "fr", "tr", "uk", "af", "ko", "it", "ga", "pt", "ru",
|
||||
"hu",
|
||||
];
|
||||
|
||||
const HERMES_RUNTIME_FOOTER_FIELDS: &[&str] =
|
||||
&["model", "context_pct", "cwd", "duration", "tokens", "cost"];
|
||||
|
||||
fn normalize_hermes_display_language(
|
||||
value: Option<String>,
|
||||
strict: bool,
|
||||
) -> Result<String, String> {
|
||||
let language = value.unwrap_or_default().trim().to_ascii_lowercase();
|
||||
let language = if language.is_empty() {
|
||||
"en".to_string()
|
||||
} else {
|
||||
language
|
||||
};
|
||||
if HERMES_DISPLAY_LANGUAGE_VALUES.contains(&language.as_str()) {
|
||||
Ok(language)
|
||||
} else if strict {
|
||||
Err("display.language 不在支持列表中".to_string())
|
||||
} else {
|
||||
Ok("en".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_hermes_display_resume(value: Option<String>, strict: bool) -> Result<String, String> {
|
||||
let mode = value.unwrap_or_default().trim().to_ascii_lowercase();
|
||||
let mode = if mode.is_empty() {
|
||||
"full".to_string()
|
||||
} else {
|
||||
mode
|
||||
};
|
||||
if matches!(mode.as_str(), "full" | "minimal") {
|
||||
Ok(mode)
|
||||
} else if strict {
|
||||
Err("display.resume_display 必须是 full 或 minimal".to_string())
|
||||
} else {
|
||||
Ok("full".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_hermes_runtime_footer_fields_text(
|
||||
value: Option<String>,
|
||||
strict: bool,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let fields = match value {
|
||||
Some(value) => {
|
||||
let text = value.trim().to_string();
|
||||
if text.contains('\n') || text.contains(',') {
|
||||
text.split(['\n', ','])
|
||||
.map(str::trim)
|
||||
.filter(|item| !item.is_empty())
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
} else if text.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
vec![text]
|
||||
}
|
||||
}
|
||||
None => Vec::new(),
|
||||
};
|
||||
let fields = if fields.is_empty() {
|
||||
vec![
|
||||
"model".to_string(),
|
||||
"context_pct".to_string(),
|
||||
"cwd".to_string(),
|
||||
]
|
||||
} else {
|
||||
fields
|
||||
};
|
||||
if let Some(invalid) = fields
|
||||
.iter()
|
||||
.find(|item| !HERMES_RUNTIME_FOOTER_FIELDS.contains(&item.as_str()))
|
||||
{
|
||||
if strict {
|
||||
return Err(format!(
|
||||
"display.runtime_footer.fields 包含不支持的字段: {invalid}"
|
||||
));
|
||||
}
|
||||
return Ok(vec![
|
||||
"model".to_string(),
|
||||
"context_pct".to_string(),
|
||||
"cwd".to_string(),
|
||||
]);
|
||||
}
|
||||
Ok(fields)
|
||||
}
|
||||
|
||||
fn normalize_hermes_runtime_footer_fields(
|
||||
value: Option<&serde_yaml::Value>,
|
||||
strict: bool,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let fields = match value {
|
||||
Some(serde_yaml::Value::Sequence(items)) => items
|
||||
.iter()
|
||||
.filter_map(|item| item.as_str().map(str::trim))
|
||||
.filter(|item| !item.is_empty())
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>(),
|
||||
Some(serde_yaml::Value::String(text)) => text
|
||||
.split(['\n', ','])
|
||||
.map(str::trim)
|
||||
.filter(|item| !item.is_empty())
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
normalize_hermes_runtime_footer_fields_text(
|
||||
if fields.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(fields.join("\n"))
|
||||
},
|
||||
strict,
|
||||
)
|
||||
}
|
||||
|
||||
fn build_hermes_display_config_values(config: &serde_yaml::Value) -> Value {
|
||||
let root = config.as_mapping();
|
||||
let display = root.and_then(|map| yaml_get_mapping(map, "display"));
|
||||
let runtime_footer = display.and_then(|map| yaml_get_mapping(map, "runtime_footer"));
|
||||
let runtime_footer_fields = normalize_hermes_runtime_footer_fields(
|
||||
runtime_footer.and_then(|map| yaml_get(map, "fields")),
|
||||
false,
|
||||
)
|
||||
.unwrap_or_else(|_| {
|
||||
vec![
|
||||
"model".to_string(),
|
||||
"context_pct".to_string(),
|
||||
"cwd".to_string(),
|
||||
]
|
||||
});
|
||||
|
||||
serde_json::json!({
|
||||
"displayToolProgress": normalize_hermes_display_tool_progress(
|
||||
display.and_then(|map| yaml_string_field(map, "tool_progress")),
|
||||
false,
|
||||
"display.tool_progress",
|
||||
).unwrap_or_else(|_| "all".to_string()),
|
||||
"displayToolProgressCommand": display.and_then(|map| yaml_bool_field(map, "tool_progress_command")).unwrap_or(false),
|
||||
"displayInterimAssistantMessages": display.and_then(|map| yaml_bool_field(map, "interim_assistant_messages")).unwrap_or(true),
|
||||
"displayRuntimeFooterEnabled": runtime_footer.and_then(|map| yaml_bool_field(map, "enabled")).unwrap_or(false),
|
||||
"displayRuntimeFooterFields": runtime_footer_fields.join("\n"),
|
||||
"displayFileMutationVerifier": display.and_then(|map| yaml_bool_field(map, "file_mutation_verifier")).unwrap_or(true),
|
||||
"displayLanguage": normalize_hermes_display_language(
|
||||
display.and_then(|map| yaml_string_field(map, "language")),
|
||||
false,
|
||||
).unwrap_or_else(|_| "en".to_string()),
|
||||
"displayResumeDisplay": normalize_hermes_display_resume(
|
||||
display.and_then(|map| yaml_string_field(map, "resume_display")),
|
||||
false,
|
||||
).unwrap_or_else(|_| "full".to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
fn merge_hermes_display_config(config: &mut serde_yaml::Value, form: &Value) -> Result<(), String> {
|
||||
let current = build_hermes_display_config_values(config);
|
||||
let tool_progress = normalize_hermes_display_tool_progress(
|
||||
form_string(form, "displayToolProgress").or_else(|| {
|
||||
current["displayToolProgress"]
|
||||
.as_str()
|
||||
.map(ToString::to_string)
|
||||
}),
|
||||
true,
|
||||
"display.tool_progress",
|
||||
)?;
|
||||
let runtime_footer_fields = normalize_hermes_runtime_footer_fields_text(
|
||||
form.get("displayRuntimeFooterFields")
|
||||
.and_then(|value| value.as_str().map(ToString::to_string))
|
||||
.or_else(|| {
|
||||
current["displayRuntimeFooterFields"]
|
||||
.as_str()
|
||||
.map(ToString::to_string)
|
||||
}),
|
||||
true,
|
||||
)?;
|
||||
|
||||
let display = yaml_child_object(ensure_yaml_object(config)?, "display")?;
|
||||
display.insert(
|
||||
yaml_key("tool_progress"),
|
||||
serde_yaml::Value::String(tool_progress),
|
||||
);
|
||||
display.insert(
|
||||
yaml_key("tool_progress_command"),
|
||||
serde_yaml::Value::Bool(
|
||||
form_bool(form, "displayToolProgressCommand").unwrap_or_else(|| {
|
||||
current["displayToolProgressCommand"]
|
||||
.as_bool()
|
||||
.unwrap_or(false)
|
||||
}),
|
||||
),
|
||||
);
|
||||
display.insert(
|
||||
yaml_key("interim_assistant_messages"),
|
||||
serde_yaml::Value::Bool(
|
||||
form_bool(form, "displayInterimAssistantMessages").unwrap_or_else(|| {
|
||||
current["displayInterimAssistantMessages"]
|
||||
.as_bool()
|
||||
.unwrap_or(true)
|
||||
}),
|
||||
),
|
||||
);
|
||||
display.insert(
|
||||
yaml_key("file_mutation_verifier"),
|
||||
serde_yaml::Value::Bool(
|
||||
form_bool(form, "displayFileMutationVerifier").unwrap_or_else(|| {
|
||||
current["displayFileMutationVerifier"]
|
||||
.as_bool()
|
||||
.unwrap_or(true)
|
||||
}),
|
||||
),
|
||||
);
|
||||
display.insert(
|
||||
yaml_key("language"),
|
||||
serde_yaml::Value::String(normalize_hermes_display_language(
|
||||
form_string(form, "displayLanguage")
|
||||
.or_else(|| current["displayLanguage"].as_str().map(ToString::to_string)),
|
||||
true,
|
||||
)?),
|
||||
);
|
||||
display.insert(
|
||||
yaml_key("resume_display"),
|
||||
serde_yaml::Value::String(normalize_hermes_display_resume(
|
||||
form_string(form, "displayResumeDisplay").or_else(|| {
|
||||
current["displayResumeDisplay"]
|
||||
.as_str()
|
||||
.map(ToString::to_string)
|
||||
}),
|
||||
true,
|
||||
)?),
|
||||
);
|
||||
let runtime_footer = yaml_child_object(display, "runtime_footer")?;
|
||||
runtime_footer.insert(
|
||||
yaml_key("enabled"),
|
||||
serde_yaml::Value::Bool(
|
||||
form_bool(form, "displayRuntimeFooterEnabled").unwrap_or_else(|| {
|
||||
current["displayRuntimeFooterEnabled"]
|
||||
.as_bool()
|
||||
.unwrap_or(false)
|
||||
}),
|
||||
),
|
||||
);
|
||||
runtime_footer.insert(
|
||||
yaml_key("fields"),
|
||||
serde_yaml::Value::Sequence(
|
||||
runtime_footer_fields
|
||||
.into_iter()
|
||||
.map(serde_yaml::Value::String)
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_hermes_human_delay_config_values(config: &serde_yaml::Value) -> Value {
|
||||
let root = config.as_mapping();
|
||||
let human_delay = root.and_then(|map| yaml_get_mapping(map, "human_delay"));
|
||||
@@ -5636,6 +5893,29 @@ pub fn hermes_security_config_save(form: Value) -> Result<Value, String> {
|
||||
}))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn hermes_display_config_read() -> Result<Value, String> {
|
||||
let (config_path, exists, config) = read_hermes_channel_yaml_config()?;
|
||||
Ok(serde_json::json!({
|
||||
"exists": exists,
|
||||
"configPath": config_path.to_string_lossy(),
|
||||
"values": build_hermes_display_config_values(&config),
|
||||
}))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn hermes_display_config_save(form: Value) -> Result<Value, String> {
|
||||
let (config_path, _exists, mut config) = read_hermes_channel_yaml_config()?;
|
||||
merge_hermes_display_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_display_config_values(&config),
|
||||
}))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn hermes_human_delay_config_read() -> Result<Value, String> {
|
||||
let (config_path, exists, config) = read_hermes_channel_yaml_config()?;
|
||||
@@ -11844,6 +12124,169 @@ memory:
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod hermes_display_config_tests {
|
||||
use super::{build_hermes_display_config_values, merge_hermes_display_config};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn display_values_have_upstream_defaults() {
|
||||
let config: serde_yaml::Value = serde_yaml::from_str("{}").unwrap();
|
||||
let values = build_hermes_display_config_values(&config);
|
||||
assert_eq!(values["displayToolProgress"], "all");
|
||||
assert_eq!(values["displayToolProgressCommand"], false);
|
||||
assert_eq!(values["displayInterimAssistantMessages"], true);
|
||||
assert_eq!(values["displayRuntimeFooterEnabled"], false);
|
||||
assert_eq!(
|
||||
values["displayRuntimeFooterFields"],
|
||||
"model\ncontext_pct\ncwd"
|
||||
);
|
||||
assert_eq!(values["displayFileMutationVerifier"], true);
|
||||
assert_eq!(values["displayLanguage"], "en");
|
||||
assert_eq!(values["displayResumeDisplay"], "full");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_values_normalize_existing_fields() {
|
||||
let config: serde_yaml::Value = serde_yaml::from_str(
|
||||
r#"
|
||||
display:
|
||||
tool_progress: VERBOSE
|
||||
tool_progress_command: true
|
||||
interim_assistant_messages: false
|
||||
runtime_footer:
|
||||
enabled: true
|
||||
fields:
|
||||
- model
|
||||
- duration
|
||||
- cost
|
||||
file_mutation_verifier: false
|
||||
language: ZH
|
||||
resume_display: minimal
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let values = build_hermes_display_config_values(&config);
|
||||
assert_eq!(values["displayToolProgress"], "verbose");
|
||||
assert_eq!(values["displayToolProgressCommand"], true);
|
||||
assert_eq!(values["displayInterimAssistantMessages"], false);
|
||||
assert_eq!(values["displayRuntimeFooterEnabled"], true);
|
||||
assert_eq!(
|
||||
values["displayRuntimeFooterFields"],
|
||||
"model\nduration\ncost"
|
||||
);
|
||||
assert_eq!(values["displayFileMutationVerifier"], false);
|
||||
assert_eq!(values["displayLanguage"], "zh");
|
||||
assert_eq!(values["displayResumeDisplay"], "minimal");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_display_config_preserves_unknown_fields() {
|
||||
let mut config: serde_yaml::Value = serde_yaml::from_str(
|
||||
r#"
|
||||
model:
|
||||
provider: anthropic
|
||||
display:
|
||||
skin: midnight
|
||||
runtime_footer:
|
||||
enabled: false
|
||||
custom_flag: keep-footer
|
||||
platforms:
|
||||
telegram:
|
||||
tool_progress: new
|
||||
memory:
|
||||
memory_enabled: true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
merge_hermes_display_config(
|
||||
&mut config,
|
||||
&json!({
|
||||
"displayToolProgress": "off",
|
||||
"displayToolProgressCommand": true,
|
||||
"displayInterimAssistantMessages": false,
|
||||
"displayRuntimeFooterEnabled": true,
|
||||
"displayRuntimeFooterFields": "model\ncontext_pct\nduration",
|
||||
"displayFileMutationVerifier": true,
|
||||
"displayLanguage": "zh-hant",
|
||||
"displayResumeDisplay": "minimal",
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(config["model"]["provider"].as_str(), Some("anthropic"));
|
||||
assert_eq!(config["memory"]["memory_enabled"].as_bool(), Some(true));
|
||||
assert_eq!(config["display"]["skin"].as_str(), Some("midnight"));
|
||||
assert_eq!(
|
||||
config["display"]["platforms"]["telegram"]["tool_progress"].as_str(),
|
||||
Some("new")
|
||||
);
|
||||
assert_eq!(config["display"]["tool_progress"].as_str(), Some("off"));
|
||||
assert_eq!(
|
||||
config["display"]["tool_progress_command"].as_bool(),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
config["display"]["interim_assistant_messages"].as_bool(),
|
||||
Some(false)
|
||||
);
|
||||
assert_eq!(
|
||||
config["display"]["runtime_footer"]["enabled"].as_bool(),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
config["display"]["runtime_footer"]["custom_flag"].as_str(),
|
||||
Some("keep-footer")
|
||||
);
|
||||
assert_eq!(
|
||||
config["display"]["runtime_footer"]["fields"]
|
||||
.as_sequence()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter_map(|item| item.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["model", "context_pct", "duration"]
|
||||
);
|
||||
assert_eq!(
|
||||
config["display"]["file_mutation_verifier"].as_bool(),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(config["display"]["language"].as_str(), Some("zh-hant"));
|
||||
assert_eq!(
|
||||
config["display"]["resume_display"].as_str(),
|
||||
Some("minimal")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_display_config_rejects_invalid_values() {
|
||||
let mut config = serde_yaml::Value::Mapping(serde_yaml::Mapping::new());
|
||||
let err = merge_hermes_display_config(
|
||||
&mut config,
|
||||
&json!({ "displayToolProgress": "everything" }),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(err.contains("display.tool_progress"));
|
||||
|
||||
let err =
|
||||
merge_hermes_display_config(&mut config, &json!({ "displayResumeDisplay": "compact" }))
|
||||
.unwrap_err();
|
||||
assert!(err.contains("display.resume_display"));
|
||||
|
||||
let err = merge_hermes_display_config(&mut config, &json!({ "displayLanguage": "cn" }))
|
||||
.unwrap_err();
|
||||
assert!(err.contains("display.language"));
|
||||
|
||||
let err = merge_hermes_display_config(
|
||||
&mut config,
|
||||
&json!({ "displayRuntimeFooterFields": "model\npassword" }),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(err.contains("display.runtime_footer.fields"));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod hermes_security_config_tests {
|
||||
use super::{build_hermes_security_config_values, merge_hermes_security_config};
|
||||
|
||||
@@ -273,6 +273,8 @@ pub fn run() {
|
||||
hermes::hermes_unauthorized_dm_config_save,
|
||||
hermes::hermes_security_config_read,
|
||||
hermes::hermes_security_config_save,
|
||||
hermes::hermes_display_config_read,
|
||||
hermes::hermes_display_config_save,
|
||||
hermes::hermes_human_delay_config_read,
|
||||
hermes::hermes_human_delay_config_save,
|
||||
hermes::hermes_streaming_config_read,
|
||||
|
||||
@@ -63,6 +63,17 @@ const SECURITY_DEFAULTS = {
|
||||
tirithFailOpen: true,
|
||||
}
|
||||
|
||||
const DISPLAY_DEFAULTS = {
|
||||
displayToolProgress: 'all',
|
||||
displayToolProgressCommand: false,
|
||||
displayInterimAssistantMessages: true,
|
||||
displayRuntimeFooterEnabled: false,
|
||||
displayRuntimeFooterFields: 'model\ncontext_pct\ncwd',
|
||||
displayFileMutationVerifier: true,
|
||||
displayLanguage: 'en',
|
||||
displayResumeDisplay: 'full',
|
||||
}
|
||||
|
||||
const HUMAN_DELAY_DEFAULTS = {
|
||||
humanDelayMode: 'off',
|
||||
humanDelayMinMs: 800,
|
||||
@@ -109,6 +120,9 @@ 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']
|
||||
const DISPLAY_TOOL_PROGRESS_VALUES = ['off', 'new', 'all', 'verbose']
|
||||
const DISPLAY_LANGUAGE_VALUES = ['en', 'zh', 'zh-hant', 'ja', 'de', 'es', 'fr', 'tr', 'uk', 'af', 'ko', 'it', 'ga', 'pt', 'ru', 'hu']
|
||||
const DISPLAY_RESUME_VALUES = ['full', 'minimal']
|
||||
const HUMAN_DELAY_MODES = ['off', 'natural', 'custom']
|
||||
|
||||
export function render() {
|
||||
@@ -124,6 +138,7 @@ export function render() {
|
||||
let quickCommandsValues = { ...QUICK_COMMANDS_DEFAULTS }
|
||||
let unauthorizedDmValues = { ...UNAUTHORIZED_DM_DEFAULTS }
|
||||
let securityValues = { ...SECURITY_DEFAULTS }
|
||||
let displayValues = { ...DISPLAY_DEFAULTS }
|
||||
let humanDelayValues = { ...HUMAN_DELAY_DEFAULTS }
|
||||
let streamingValues = { ...STREAMING_DEFAULTS }
|
||||
let executionLimitsValues = { ...EXECUTION_LIMITS_DEFAULTS }
|
||||
@@ -137,6 +152,7 @@ export function render() {
|
||||
let quickCommandsLoading = true
|
||||
let unauthorizedDmLoading = true
|
||||
let securityLoading = true
|
||||
let displayLoading = true
|
||||
let humanDelayLoading = true
|
||||
let streamingLoading = true
|
||||
let executionLimitsLoading = true
|
||||
@@ -150,6 +166,7 @@ export function render() {
|
||||
let quickCommandsSaving = false
|
||||
let unauthorizedDmSaving = false
|
||||
let securitySaving = false
|
||||
let displaySaving = false
|
||||
let humanDelaySaving = false
|
||||
let streamingSaving = false
|
||||
let executionLimitsSaving = false
|
||||
@@ -163,6 +180,7 @@ export function render() {
|
||||
let quickCommandsError = null
|
||||
let unauthorizedDmError = null
|
||||
let securityError = null
|
||||
let displayError = null
|
||||
let humanDelayError = null
|
||||
let streamingError = null
|
||||
let executionLimitsError = null
|
||||
@@ -177,7 +195,7 @@ export function render() {
|
||||
}
|
||||
|
||||
function isBusy() {
|
||||
return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || unauthorizedDmLoading || securityLoading || humanDelayLoading || streamingLoading || executionLimitsLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || securitySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || terminalSaving
|
||||
return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || unauthorizedDmLoading || securityLoading || displayLoading || humanDelayLoading || streamingLoading || executionLimitsLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || terminalSaving
|
||||
}
|
||||
|
||||
function option(labelKey, value, selected) {
|
||||
@@ -533,6 +551,70 @@ export function render() {
|
||||
`
|
||||
}
|
||||
|
||||
function renderDisplayConfigPanel() {
|
||||
const disabled = loading || saving || displayLoading || displaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || securitySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || terminalSaving
|
||||
return `
|
||||
<div class="hm-panel hm-config-runtime-panel hm-config-display-panel">
|
||||
<div class="hm-panel-header">
|
||||
<div>
|
||||
<div class="hm-panel-title">${t('engine.hermesDisplayConfigTitle')}</div>
|
||||
<div class="hm-channel-panel-desc">${t('engine.hermesDisplayConfigDesc')}</div>
|
||||
</div>
|
||||
<div class="hm-panel-actions">
|
||||
<span class="hm-muted">${displaySaving ? t('engine.hermesConfigStatusSaving') : displayLoading ? t('engine.hermesConfigStatusLoading') : t('engine.hermesDisplayConfigStatusReady')}</span>
|
||||
<button class="hm-btn hm-btn--cta hm-btn--sm" id="hm-display-save" ${disabled ? 'disabled' : ''}>${t('engine.hermesDisplayConfigSave')}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hm-panel-body">
|
||||
${renderError(displayError)}
|
||||
<div class="hm-config-runtime-grid hm-config-display-grid">
|
||||
<label class="hm-field">
|
||||
<span class="hm-field-label">${t('engine.hermesDisplayConfigToolProgress')}</span>
|
||||
<select id="hm-display-tool-progress" class="hm-input" ${disabled ? 'disabled' : ''}>
|
||||
${DISPLAY_TOOL_PROGRESS_VALUES.map(mode => option(`engine.hermesDisplayConfigToolProgress_${mode}`, mode, displayValues.displayToolProgress)).join('')}
|
||||
</select>
|
||||
</label>
|
||||
<label class="hm-field">
|
||||
<span class="hm-field-label">${t('engine.hermesDisplayConfigLanguage')}</span>
|
||||
<select id="hm-display-language" class="hm-input" ${disabled ? 'disabled' : ''}>
|
||||
${DISPLAY_LANGUAGE_VALUES.map(mode => option(`engine.hermesDisplayConfigLanguage_${mode}`, mode, displayValues.displayLanguage)).join('')}
|
||||
</select>
|
||||
</label>
|
||||
<label class="hm-field">
|
||||
<span class="hm-field-label">${t('engine.hermesDisplayConfigResumeDisplay')}</span>
|
||||
<select id="hm-display-resume-display" class="hm-input" ${disabled ? 'disabled' : ''}>
|
||||
${DISPLAY_RESUME_VALUES.map(mode => option(`engine.hermesDisplayConfigResumeDisplay_${mode}`, mode, displayValues.displayResumeDisplay)).join('')}
|
||||
</select>
|
||||
</label>
|
||||
<label class="hm-field">
|
||||
<span class="hm-field-label">${t('engine.hermesDisplayConfigRuntimeFooterFields')}</span>
|
||||
<textarea id="hm-display-runtime-footer-fields" class="hm-input" ${disabled ? 'disabled' : ''} style="min-height:96px;resize:vertical">${esc(displayValues.displayRuntimeFooterFields)}</textarea>
|
||||
</label>
|
||||
</div>
|
||||
<div class="hm-config-check-grid">
|
||||
<label class="hm-channel-check">
|
||||
<input id="hm-display-tool-progress-command" type="checkbox" ${displayValues.displayToolProgressCommand ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
|
||||
<span>${t('engine.hermesDisplayConfigToolProgressCommand')}</span>
|
||||
</label>
|
||||
<label class="hm-channel-check">
|
||||
<input id="hm-display-interim-assistant-messages" type="checkbox" ${displayValues.displayInterimAssistantMessages ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
|
||||
<span>${t('engine.hermesDisplayConfigInterimAssistantMessages')}</span>
|
||||
</label>
|
||||
<label class="hm-channel-check">
|
||||
<input id="hm-display-runtime-footer-enabled" type="checkbox" ${displayValues.displayRuntimeFooterEnabled ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
|
||||
<span>${t('engine.hermesDisplayConfigRuntimeFooterEnabled')}</span>
|
||||
</label>
|
||||
<label class="hm-channel-check">
|
||||
<input id="hm-display-file-mutation-verifier" type="checkbox" ${displayValues.displayFileMutationVerifier ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
|
||||
<span>${t('engine.hermesDisplayConfigFileMutationVerifier')}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="hm-channel-footnote">${t('engine.hermesDisplayConfigFootnote')}</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function renderHumanDelayConfigPanel() {
|
||||
const disabled = loading || saving || humanDelayLoading || humanDelaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || securitySaving || streamingSaving || executionLimitsSaving || terminalSaving
|
||||
return `
|
||||
@@ -791,6 +873,7 @@ export function render() {
|
||||
${renderQuickCommandsConfigPanel()}
|
||||
${renderUnauthorizedDmConfigPanel()}
|
||||
${renderSecurityConfigPanel()}
|
||||
${renderDisplayConfigPanel()}
|
||||
${renderHumanDelayConfigPanel()}
|
||||
|
||||
<div class="hm-panel">
|
||||
@@ -819,6 +902,7 @@ export function render() {
|
||||
el.querySelector('#hm-quick-commands-save')?.addEventListener('click', saveQuickCommandsConfig)
|
||||
el.querySelector('#hm-unauthorized-dm-save')?.addEventListener('click', saveUnauthorizedDmConfig)
|
||||
el.querySelector('#hm-security-save')?.addEventListener('click', saveSecurityConfig)
|
||||
el.querySelector('#hm-display-save')?.addEventListener('click', saveDisplayConfig)
|
||||
el.querySelector('#hm-human-delay-save')?.addEventListener('click', saveHumanDelayConfig)
|
||||
el.querySelector('#hm-streaming-save')?.addEventListener('click', saveStreaming)
|
||||
el.querySelector('#hm-execution-limits-save')?.addEventListener('click', saveExecutionLimits)
|
||||
@@ -870,6 +954,11 @@ export function render() {
|
||||
securityValues = { ...SECURITY_DEFAULTS, ...(data?.values || {}) }
|
||||
}
|
||||
|
||||
async function loadDisplayConfig() {
|
||||
const data = await api.hermesDisplayConfigRead()
|
||||
displayValues = { ...DISPLAY_DEFAULTS, ...(data?.values || {}) }
|
||||
}
|
||||
|
||||
async function loadHumanDelayConfig() {
|
||||
const data = await api.hermesHumanDelayConfigRead()
|
||||
humanDelayValues = { ...HUMAN_DELAY_DEFAULTS, ...(data?.values || {}) }
|
||||
@@ -900,6 +989,7 @@ export function render() {
|
||||
quickCommandsLoading = true
|
||||
unauthorizedDmLoading = true
|
||||
securityLoading = true
|
||||
displayLoading = true
|
||||
humanDelayLoading = true
|
||||
streamingLoading = true
|
||||
executionLimitsLoading = true
|
||||
@@ -913,6 +1003,7 @@ export function render() {
|
||||
quickCommandsError = null
|
||||
unauthorizedDmError = null
|
||||
securityError = null
|
||||
displayError = null
|
||||
humanDelayError = null
|
||||
streamingError = null
|
||||
executionLimitsError = null
|
||||
@@ -1013,6 +1104,14 @@ export function render() {
|
||||
securityLoading = false
|
||||
draw()
|
||||
}
|
||||
try {
|
||||
await loadDisplayConfig()
|
||||
} catch (err) {
|
||||
displayError = humanizeError(err, t('engine.hermesDisplayConfigLoadFailed') || 'Load display config failed')
|
||||
} finally {
|
||||
displayLoading = false
|
||||
draw()
|
||||
}
|
||||
try {
|
||||
await loadHumanDelayConfig()
|
||||
} catch (err) {
|
||||
@@ -1066,6 +1165,9 @@ export function render() {
|
||||
try {
|
||||
await loadSecurityConfig()
|
||||
} catch {}
|
||||
try {
|
||||
await loadDisplayConfig()
|
||||
} catch {}
|
||||
try {
|
||||
await loadHumanDelayConfig()
|
||||
} catch {}
|
||||
@@ -1312,6 +1414,38 @@ export function render() {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDisplayConfig() {
|
||||
const form = {
|
||||
displayToolProgress: el.querySelector('#hm-display-tool-progress')?.value || 'all',
|
||||
displayToolProgressCommand: !!el.querySelector('#hm-display-tool-progress-command')?.checked,
|
||||
displayInterimAssistantMessages: !!el.querySelector('#hm-display-interim-assistant-messages')?.checked,
|
||||
displayRuntimeFooterEnabled: !!el.querySelector('#hm-display-runtime-footer-enabled')?.checked,
|
||||
displayRuntimeFooterFields: el.querySelector('#hm-display-runtime-footer-fields')?.value || 'model\ncontext_pct\ncwd',
|
||||
displayFileMutationVerifier: !!el.querySelector('#hm-display-file-mutation-verifier')?.checked,
|
||||
displayLanguage: el.querySelector('#hm-display-language')?.value || 'en',
|
||||
displayResumeDisplay: el.querySelector('#hm-display-resume-display')?.value || 'full',
|
||||
}
|
||||
displaySaving = true
|
||||
displayError = null
|
||||
draw()
|
||||
try {
|
||||
const result = await api.hermesDisplayConfigSave(form)
|
||||
displayValues = { ...DISPLAY_DEFAULTS, ...(result?.values || form) }
|
||||
await refreshRawAfterStructuredSave()
|
||||
const backup = result?.backup || ''
|
||||
toast({
|
||||
message: t('engine.hermesDisplayConfigSaveSuccess'),
|
||||
hint: backup ? t('engine.hermesConfigBackupHint', { path: backup }) : '',
|
||||
}, 'success')
|
||||
} catch (err) {
|
||||
displayError = humanizeError(err, t('engine.hermesDisplayConfigSaveFailed') || 'Save display config failed')
|
||||
toast(displayError, 'error')
|
||||
} finally {
|
||||
displaySaving = false
|
||||
draw()
|
||||
}
|
||||
}
|
||||
|
||||
async function saveHumanDelayConfig() {
|
||||
const form = {
|
||||
humanDelayMode: el.querySelector('#hm-human-delay-mode')?.value || 'off',
|
||||
|
||||
@@ -525,6 +525,8 @@ export const api = {
|
||||
hermesUnauthorizedDmConfigSave: (form) => invoke('hermes_unauthorized_dm_config_save', { form }),
|
||||
hermesSecurityConfigRead: () => invoke('hermes_security_config_read'),
|
||||
hermesSecurityConfigSave: (form) => invoke('hermes_security_config_save', { form }),
|
||||
hermesDisplayConfigRead: () => invoke('hermes_display_config_read'),
|
||||
hermesDisplayConfigSave: (form) => invoke('hermes_display_config_save', { form }),
|
||||
hermesHumanDelayConfigRead: () => invoke('hermes_human_delay_config_read'),
|
||||
hermesHumanDelayConfigSave: (form) => invoke('hermes_human_delay_config_save', { form }),
|
||||
hermesStreamingConfigRead: () => invoke('hermes_streaming_config_read'),
|
||||
|
||||
@@ -640,6 +640,44 @@ export default {
|
||||
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 中單獨設定。'),
|
||||
hermesDisplayConfigTitle: _('全局显示与可靠性', 'Global display and reliability', '全域顯示與可靠性'),
|
||||
hermesDisplayConfigDesc: _('控制消息平台和 CLI 的默认进度展示、静态提示语言、运行信息页脚,以及文件写入失败校验。', 'Control default progress display, static prompt language, runtime footer, and failed file-mutation verification for messaging platforms and CLI.', '控制訊息平台和 CLI 的預設進度顯示、靜態提示語言、執行資訊頁腳,以及檔案寫入失敗校驗。'),
|
||||
hermesDisplayConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'),
|
||||
hermesDisplayConfigSave: _('保存显示设置', 'Save display settings', '儲存顯示設定'),
|
||||
hermesDisplayConfigSaveSuccess: _('显示与可靠性配置已保存,建议重启 Hermes Gateway 生效', 'Display and reliability settings saved. Restart Hermes Gateway to take effect.', '顯示與可靠性設定已儲存,建議重啟 Hermes Gateway 生效'),
|
||||
hermesDisplayConfigLoadFailed: _('加载显示与可靠性配置失败', 'Load display and reliability settings failed', '載入顯示與可靠性設定失敗'),
|
||||
hermesDisplayConfigSaveFailed: _('保存显示与可靠性配置失败', 'Save display and reliability settings failed', '儲存顯示與可靠性設定失敗'),
|
||||
hermesDisplayConfigToolProgress: _('默认工具进度', 'Default tool progress', '預設工具進度'),
|
||||
hermesDisplayConfigToolProgress_off: _('关闭', 'Off', '關閉'),
|
||||
hermesDisplayConfigToolProgress_new: _('工具变化时显示', 'Only when tool changes', '工具變化時顯示'),
|
||||
hermesDisplayConfigToolProgress_all: _('显示每次工具调用', 'Show every tool call', '顯示每次工具呼叫'),
|
||||
hermesDisplayConfigToolProgress_verbose: _('详细显示参数和结果', 'Verbose args and results', '詳細顯示參數與結果'),
|
||||
hermesDisplayConfigToolProgressCommand: _('在消息平台启用 /verbose 命令', 'Enable /verbose on messaging platforms', '在訊息平台啟用 /verbose 命令'),
|
||||
hermesDisplayConfigInterimAssistantMessages: _('发送中途状态消息', 'Send interim assistant updates', '傳送中途狀態訊息'),
|
||||
hermesDisplayConfigRuntimeFooterEnabled: _('在最终回复追加运行信息', 'Append runtime footer to final replies', '在最終回覆追加執行資訊'),
|
||||
hermesDisplayConfigRuntimeFooterFields: _('运行信息字段(每行一个)', 'Runtime footer fields, one per line', '執行資訊欄位(每行一個)'),
|
||||
hermesDisplayConfigFileMutationVerifier: _('启用文件写入失败校验', 'Enable failed file-mutation verifier', '啟用檔案寫入失敗校驗'),
|
||||
hermesDisplayConfigLanguage: _('静态提示语言', 'Static prompt language', '靜態提示語言'),
|
||||
hermesDisplayConfigLanguage_en: _('英语', 'English', '英語'),
|
||||
hermesDisplayConfigLanguage_zh: _('简体中文', 'Simplified Chinese', '簡體中文'),
|
||||
'hermesDisplayConfigLanguage_zh-hant': _('繁体中文', 'Traditional Chinese', '繁體中文'),
|
||||
hermesDisplayConfigLanguage_ja: _('日语', 'Japanese', '日語'),
|
||||
hermesDisplayConfigLanguage_de: _('德语', 'German', '德語'),
|
||||
hermesDisplayConfigLanguage_es: _('西班牙语', 'Spanish', '西班牙語'),
|
||||
hermesDisplayConfigLanguage_fr: _('法语', 'French', '法語'),
|
||||
hermesDisplayConfigLanguage_tr: _('土耳其语', 'Turkish', '土耳其語'),
|
||||
hermesDisplayConfigLanguage_uk: _('乌克兰语', 'Ukrainian', '烏克蘭語'),
|
||||
hermesDisplayConfigLanguage_af: _('南非荷兰语', 'Afrikaans', '南非荷蘭語'),
|
||||
hermesDisplayConfigLanguage_ko: _('韩语', 'Korean', '韓語'),
|
||||
hermesDisplayConfigLanguage_it: _('意大利语', 'Italian', '義大利語'),
|
||||
hermesDisplayConfigLanguage_ga: _('爱尔兰语', 'Irish', '愛爾蘭語'),
|
||||
hermesDisplayConfigLanguage_pt: _('葡萄牙语', 'Portuguese', '葡萄牙語'),
|
||||
hermesDisplayConfigLanguage_ru: _('俄语', 'Russian', '俄語'),
|
||||
hermesDisplayConfigLanguage_hu: _('匈牙利语', 'Hungarian', '匈牙利語'),
|
||||
hermesDisplayConfigResumeDisplay: _('恢复会话展示', 'Resume display', '恢復會話顯示'),
|
||||
hermesDisplayConfigResumeDisplay_full: _('显示完整上下文', 'Show full context', '顯示完整上下文'),
|
||||
hermesDisplayConfigResumeDisplay_minimal: _('仅显示一行摘要', 'Show one-line summary', '僅顯示一行摘要'),
|
||||
hermesDisplayConfigFootnote: _('这里写入全局 display 配置;平台级覆盖仍在渠道页管理。display.streaming 是 CLI-only,本面板不会把它误写成 Gateway 全局流式设置。运行信息字段支持 model、context_pct、cwd、duration、tokens、cost。', 'This writes global display settings; per-platform overrides remain in channel settings. display.streaming is CLI-only, so this panel does not write it as a global Gateway streaming setting. Runtime footer fields support model, context_pct, cwd, duration, tokens, and cost.', '這裡寫入全域 display 設定;平台級覆蓋仍在頻道頁管理。display.streaming 是 CLI-only,本面板不會把它誤寫成 Gateway 全域串流設定。執行資訊欄位支援 model、context_pct、cwd、duration、tokens、cost。'),
|
||||
hermesHumanDelayConfigTitle: _('响应节奏', 'Response pacing', '回應節奏'),
|
||||
hermesHumanDelayConfigDesc: _('控制消息平台回复分块之间的等待时间,降低刷屏或模拟更自然发送节奏。', 'Control the wait time between reply chunks on messaging platforms to reduce flooding or mimic a more natural sending rhythm.', '控制訊息平台回覆分塊之間的等待時間,降低刷屏或模擬更自然的傳送節奏。'),
|
||||
hermesHumanDelayConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'),
|
||||
|
||||
@@ -90,6 +90,22 @@ test('Hermes 配置页会暴露响应节奏结构化配置字段', () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('Hermes 配置页会暴露全局显示与可靠性结构化配置字段', () => {
|
||||
for (const id of [
|
||||
'hm-display-save',
|
||||
'hm-display-tool-progress',
|
||||
'hm-display-tool-progress-command',
|
||||
'hm-display-interim-assistant-messages',
|
||||
'hm-display-runtime-footer-enabled',
|
||||
'hm-display-runtime-footer-fields',
|
||||
'hm-display-file-mutation-verifier',
|
||||
'hm-display-language',
|
||||
'hm-display-resume-display',
|
||||
]) {
|
||||
assert.match(source, new RegExp(`id="${id}"`), `缺少 ${id}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('Hermes 配置页会暴露网关流式结构化配置字段', () => {
|
||||
for (const id of [
|
||||
'hm-streaming-save',
|
||||
@@ -153,6 +169,7 @@ test('Hermes 配置页新增结构化配置不会暴露翻译 key', () => {
|
||||
key.includes('UnauthorizedDmConfig') ||
|
||||
key.includes('SecurityConfig') ||
|
||||
key.includes('HumanDelayConfig') ||
|
||||
key.includes('DisplayConfig') ||
|
||||
key.includes('StreamingConfig') ||
|
||||
key.includes('ExecutionLimits') ||
|
||||
key.includes('TerminalConfig')
|
||||
|
||||
107
tests/hermes-display-config.test.js
Normal file
107
tests/hermes-display-config.test.js
Normal file
@@ -0,0 +1,107 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
buildHermesDisplayConfigValues,
|
||||
mergeHermesDisplayConfig,
|
||||
} from '../scripts/dev-api.js'
|
||||
|
||||
test('Hermes 显示配置读取会提供上游默认值', () => {
|
||||
const values = buildHermesDisplayConfigValues({})
|
||||
|
||||
assert.deepEqual(values, {
|
||||
displayToolProgress: 'all',
|
||||
displayToolProgressCommand: false,
|
||||
displayInterimAssistantMessages: true,
|
||||
displayRuntimeFooterEnabled: false,
|
||||
displayRuntimeFooterFields: 'model\ncontext_pct\ncwd',
|
||||
displayFileMutationVerifier: true,
|
||||
displayLanguage: 'en',
|
||||
displayResumeDisplay: 'full',
|
||||
})
|
||||
})
|
||||
|
||||
test('Hermes 显示配置读取会规范化已有字段', () => {
|
||||
const values = buildHermesDisplayConfigValues({
|
||||
display: {
|
||||
tool_progress: 'VERBOSE',
|
||||
tool_progress_command: true,
|
||||
interim_assistant_messages: false,
|
||||
runtime_footer: {
|
||||
enabled: true,
|
||||
fields: ['model', 'duration', 'cost'],
|
||||
},
|
||||
file_mutation_verifier: false,
|
||||
language: 'ZH',
|
||||
resume_display: 'minimal',
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(values.displayToolProgress, 'verbose')
|
||||
assert.equal(values.displayToolProgressCommand, true)
|
||||
assert.equal(values.displayInterimAssistantMessages, false)
|
||||
assert.equal(values.displayRuntimeFooterEnabled, true)
|
||||
assert.equal(values.displayRuntimeFooterFields, 'model\nduration\ncost')
|
||||
assert.equal(values.displayFileMutationVerifier, false)
|
||||
assert.equal(values.displayLanguage, 'zh')
|
||||
assert.equal(values.displayResumeDisplay, 'minimal')
|
||||
})
|
||||
|
||||
test('Hermes 显示配置保存会保留未知 YAML 并写入 display', () => {
|
||||
const next = mergeHermesDisplayConfig({
|
||||
model: { provider: 'anthropic' },
|
||||
display: {
|
||||
skin: 'midnight',
|
||||
runtime_footer: {
|
||||
enabled: false,
|
||||
custom_flag: 'keep-footer',
|
||||
},
|
||||
platforms: {
|
||||
telegram: { tool_progress: 'new' },
|
||||
},
|
||||
},
|
||||
memory: { memory_enabled: true },
|
||||
}, {
|
||||
displayToolProgress: 'off',
|
||||
displayToolProgressCommand: 'true',
|
||||
displayInterimAssistantMessages: false,
|
||||
displayRuntimeFooterEnabled: true,
|
||||
displayRuntimeFooterFields: 'model\ncontext_pct\nduration',
|
||||
displayFileMutationVerifier: true,
|
||||
displayLanguage: 'zh-hant',
|
||||
displayResumeDisplay: 'minimal',
|
||||
})
|
||||
|
||||
assert.deepEqual(next.model, { provider: 'anthropic' })
|
||||
assert.deepEqual(next.memory, { memory_enabled: true })
|
||||
assert.equal(next.display.skin, 'midnight')
|
||||
assert.deepEqual(next.display.platforms.telegram, { tool_progress: 'new' })
|
||||
assert.equal(next.display.tool_progress, 'off')
|
||||
assert.equal(next.display.tool_progress_command, true)
|
||||
assert.equal(next.display.interim_assistant_messages, false)
|
||||
assert.equal(next.display.runtime_footer.enabled, true)
|
||||
assert.deepEqual(next.display.runtime_footer.fields, ['model', 'context_pct', 'duration'])
|
||||
assert.equal(next.display.runtime_footer.custom_flag, 'keep-footer')
|
||||
assert.equal(next.display.file_mutation_verifier, true)
|
||||
assert.equal(next.display.language, 'zh-hant')
|
||||
assert.equal(next.display.resume_display, 'minimal')
|
||||
})
|
||||
|
||||
test('Hermes 显示配置保存会拒绝非法枚举和页脚字段', () => {
|
||||
assert.throws(
|
||||
() => mergeHermesDisplayConfig({}, { displayToolProgress: 'everything' }),
|
||||
/display\.tool_progress/,
|
||||
)
|
||||
assert.throws(
|
||||
() => mergeHermesDisplayConfig({}, { displayResumeDisplay: 'compact' }),
|
||||
/display\.resume_display/,
|
||||
)
|
||||
assert.throws(
|
||||
() => mergeHermesDisplayConfig({}, { displayLanguage: 'cn' }),
|
||||
/display\.language/,
|
||||
)
|
||||
assert.throws(
|
||||
() => mergeHermesDisplayConfig({}, { displayRuntimeFooterFields: 'model\npassword' }),
|
||||
/display\.runtime_footer\.fields/,
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user