mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-30 04:40:18 +08:00
feat(hermes): add channel display settings form
This commit is contained in:
@@ -3322,6 +3322,8 @@ function normalizeHermesPlatform(platform) {
|
||||
|
||||
const HERMES_SESSION_RESET_MODES = new Set(['both', 'idle', 'daily', 'none'])
|
||||
const HERMES_STREAMING_TRANSPORTS = new Set(['auto', 'draft', 'edit', 'off'])
|
||||
const HERMES_DISPLAY_TOOL_PROGRESS_VALUES = new Set(['off', 'new', 'all', 'verbose'])
|
||||
const HERMES_DISPLAY_STREAMING_VALUES = new Set(['inherit', 'true', 'false'])
|
||||
|
||||
function parseHermesInteger(value, key, fallback, min, max, strict = false) {
|
||||
const raw = String(value ?? '').trim()
|
||||
@@ -3380,6 +3382,58 @@ function normalizeHermesStreamingTransport(value, strict = false) {
|
||||
return 'edit'
|
||||
}
|
||||
|
||||
function normalizeHermesDisplayToolProgress(value, strict = false, key = 'display.tool_progress') {
|
||||
const progress = String(value ?? '').trim().toLowerCase() || 'all'
|
||||
if (HERMES_DISPLAY_TOOL_PROGRESS_VALUES.has(progress)) return progress
|
||||
if (strict) throw new Error(`${key} 必须是 off、new、all 或 verbose`)
|
||||
return 'all'
|
||||
}
|
||||
|
||||
function normalizeHermesDisplayStreaming(value, strict = false, key = 'display.streaming') {
|
||||
if (typeof value === 'boolean') return value ? 'true' : 'false'
|
||||
const streaming = String(value ?? '').trim().toLowerCase() || 'inherit'
|
||||
if (HERMES_DISPLAY_STREAMING_VALUES.has(streaming)) return streaming
|
||||
if (strict) throw new Error(`${key} 必须是 inherit、true 或 false`)
|
||||
return 'inherit'
|
||||
}
|
||||
|
||||
function hermesDisplayConfigParts(config = {}, platform = '') {
|
||||
const display = config?.display && typeof config.display === 'object' && !Array.isArray(config.display)
|
||||
? config.display
|
||||
: {}
|
||||
const platforms = display.platforms && typeof display.platforms === 'object' && !Array.isArray(display.platforms)
|
||||
? display.platforms
|
||||
: {}
|
||||
const platformDisplay = platforms[platform] && typeof platforms[platform] === 'object' && !Array.isArray(platforms[platform])
|
||||
? platforms[platform]
|
||||
: {}
|
||||
return { display, platformDisplay }
|
||||
}
|
||||
|
||||
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)
|
||||
? display.tool_progress_overrides[platform]
|
||||
: undefined
|
||||
form.displayToolProgress = normalizeHermesDisplayToolProgress(
|
||||
platformDisplay.tool_progress ?? legacyToolProgress ?? display.tool_progress ?? 'all',
|
||||
false,
|
||||
)
|
||||
form.displayShowReasoning = readHermesBool(platformDisplay.show_reasoning ?? display.show_reasoning, false)
|
||||
form.displayToolPreviewLength = parseHermesInteger(
|
||||
platformDisplay.tool_preview_length ?? display.tool_preview_length,
|
||||
`display.platforms.${platform}.tool_preview_length`,
|
||||
0,
|
||||
0,
|
||||
200000,
|
||||
false,
|
||||
)
|
||||
form.displayStreaming = Object.hasOwn(platformDisplay, 'streaming')
|
||||
? normalizeHermesDisplayStreaming(platformDisplay.streaming, false)
|
||||
: 'inherit'
|
||||
form.displayCleanupProgress = readHermesBool(platformDisplay.cleanup_progress ?? display.cleanup_progress, false)
|
||||
}
|
||||
|
||||
function hermesStreamingConfigSource(root) {
|
||||
if (root.streaming && typeof root.streaming === 'object' && !Array.isArray(root.streaming)) {
|
||||
return root.streaming
|
||||
@@ -3801,6 +3855,7 @@ export function buildHermesChannelConfigValues(config = {}, envValues = {}) {
|
||||
putHermesCsv(form, extra, 'allow_from')
|
||||
putHermesCsv(form, extra, 'group_allow_from')
|
||||
}
|
||||
putHermesChannelDisplayFields(form, config, platform)
|
||||
values[platform] = form
|
||||
}
|
||||
return values
|
||||
@@ -3882,9 +3937,71 @@ function normalizeHermesChannelForm(platform, form = {}) {
|
||||
if (Object.hasOwn(normalized, key)) normalized[key] = csvToStringArray(normalized[key])
|
||||
}
|
||||
}
|
||||
if (Object.hasOwn(normalized, 'displayToolProgress')) {
|
||||
normalized.displayToolProgress = normalizeHermesDisplayToolProgress(
|
||||
normalized.displayToolProgress,
|
||||
true,
|
||||
`display.platforms.${platform}.tool_progress`,
|
||||
)
|
||||
}
|
||||
if (Object.hasOwn(normalized, 'displayShowReasoning')) {
|
||||
normalized.displayShowReasoning = normalized.displayShowReasoning === true || normalized.displayShowReasoning === 'true' || normalized.displayShowReasoning === 'on'
|
||||
}
|
||||
if (Object.hasOwn(normalized, 'displayToolPreviewLength')) {
|
||||
normalized.displayToolPreviewLength = parseHermesInteger(
|
||||
normalized.displayToolPreviewLength,
|
||||
`display.platforms.${platform}.tool_preview_length`,
|
||||
0,
|
||||
0,
|
||||
200000,
|
||||
true,
|
||||
)
|
||||
}
|
||||
if (Object.hasOwn(normalized, 'displayStreaming')) {
|
||||
normalized.displayStreaming = normalizeHermesDisplayStreaming(
|
||||
normalized.displayStreaming,
|
||||
true,
|
||||
`display.platforms.${platform}.streaming`,
|
||||
)
|
||||
}
|
||||
if (Object.hasOwn(normalized, 'displayCleanupProgress')) {
|
||||
normalized.displayCleanupProgress = normalized.displayCleanupProgress === true || normalized.displayCleanupProgress === 'true' || normalized.displayCleanupProgress === 'on'
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
function mergeHermesChannelDisplayConfig(next, platform, normalized) {
|
||||
const hasDisplayFields = [
|
||||
'displayToolProgress',
|
||||
'displayShowReasoning',
|
||||
'displayToolPreviewLength',
|
||||
'displayStreaming',
|
||||
'displayCleanupProgress',
|
||||
].some(key => Object.hasOwn(normalized, key))
|
||||
if (!hasDisplayFields) return
|
||||
const display = next.display && typeof next.display === 'object' && !Array.isArray(next.display)
|
||||
? mergeConfigsPreservingFields(next.display, {})
|
||||
: {}
|
||||
const platforms = display.platforms && typeof display.platforms === 'object' && !Array.isArray(display.platforms)
|
||||
? mergeConfigsPreservingFields(display.platforms, {})
|
||||
: {}
|
||||
const current = platforms[platform] && typeof platforms[platform] === 'object' && !Array.isArray(platforms[platform])
|
||||
? platforms[platform]
|
||||
: {}
|
||||
const platformDisplay = mergeConfigsPreservingFields(current, {})
|
||||
if (Object.hasOwn(normalized, 'displayToolProgress')) platformDisplay.tool_progress = normalized.displayToolProgress
|
||||
if (Object.hasOwn(normalized, 'displayShowReasoning')) platformDisplay.show_reasoning = !!normalized.displayShowReasoning
|
||||
if (Object.hasOwn(normalized, 'displayToolPreviewLength')) platformDisplay.tool_preview_length = normalized.displayToolPreviewLength
|
||||
if (Object.hasOwn(normalized, 'displayStreaming')) {
|
||||
if (normalized.displayStreaming === 'inherit') delete platformDisplay.streaming
|
||||
else platformDisplay.streaming = normalized.displayStreaming === 'true'
|
||||
}
|
||||
if (Object.hasOwn(normalized, 'displayCleanupProgress')) platformDisplay.cleanup_progress = !!normalized.displayCleanupProgress
|
||||
platforms[platform] = platformDisplay
|
||||
display.platforms = platforms
|
||||
next.display = display
|
||||
}
|
||||
|
||||
export function mergeHermesChannelConfig(config = {}, platform, form = {}) {
|
||||
const normalizedPlatform = normalizeHermesPlatform(platform)
|
||||
if (!normalizedPlatform) throw new Error(`不支持的 Hermes 渠道: ${platform}`)
|
||||
@@ -3986,6 +4103,7 @@ export function mergeHermesChannelConfig(config = {}, platform, form = {}) {
|
||||
setHermesExtra(entry, normalizedPlatform === 'dingtalk' ? 'allowed_chats' : 'group_allow_from', normalized.groupAllowFrom)
|
||||
}
|
||||
next.platforms[normalizedPlatform] = entry
|
||||
mergeHermesChannelDisplayConfig(next, normalizedPlatform, normalized)
|
||||
return next
|
||||
}
|
||||
|
||||
|
||||
@@ -2165,6 +2165,9 @@ const HERMES_CHANNEL_PLATFORMS: [&str; 10] = [
|
||||
"simplex",
|
||||
];
|
||||
|
||||
const HERMES_DISPLAY_TOOL_PROGRESS_VALUES: [&str; 4] = ["off", "new", "all", "verbose"];
|
||||
const HERMES_DISPLAY_STREAMING_VALUES: [&str; 3] = ["inherit", "true", "false"];
|
||||
|
||||
fn normalize_hermes_channel_platform(platform: &str) -> Option<&'static str> {
|
||||
let platform = platform.trim().to_ascii_lowercase();
|
||||
HERMES_CHANNEL_PLATFORMS
|
||||
@@ -2173,6 +2176,78 @@ fn normalize_hermes_channel_platform(platform: &str) -> Option<&'static str> {
|
||||
.find(|item| *item == platform)
|
||||
}
|
||||
|
||||
fn normalize_hermes_display_tool_progress(
|
||||
value: Option<String>,
|
||||
strict: bool,
|
||||
key: &str,
|
||||
) -> Result<String, String> {
|
||||
let progress = value.unwrap_or_default().trim().to_ascii_lowercase();
|
||||
let progress = if progress.is_empty() {
|
||||
"all".to_string()
|
||||
} else {
|
||||
progress
|
||||
};
|
||||
if HERMES_DISPLAY_TOOL_PROGRESS_VALUES.contains(&progress.as_str()) {
|
||||
Ok(progress)
|
||||
} else if strict {
|
||||
Err(format!("{key} 必须是 off、new、all 或 verbose"))
|
||||
} else {
|
||||
Ok("all".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_hermes_display_streaming_text(
|
||||
value: Option<String>,
|
||||
strict: bool,
|
||||
key: &str,
|
||||
) -> Result<String, String> {
|
||||
let streaming = value.unwrap_or_default().trim().to_ascii_lowercase();
|
||||
let streaming = if streaming.is_empty() {
|
||||
"inherit".to_string()
|
||||
} else {
|
||||
streaming
|
||||
};
|
||||
if HERMES_DISPLAY_STREAMING_VALUES.contains(&streaming.as_str()) {
|
||||
Ok(streaming)
|
||||
} else if strict {
|
||||
Err(format!("{key} 必须是 inherit、true 或 false"))
|
||||
} else {
|
||||
Ok("inherit".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_hermes_display_streaming_yaml(
|
||||
value: Option<&serde_yaml::Value>,
|
||||
strict: bool,
|
||||
key: &str,
|
||||
) -> Result<String, String> {
|
||||
if let Some(value) = value {
|
||||
if let Some(value) = value.as_bool() {
|
||||
return Ok(if value { "true" } else { "false" }.to_string());
|
||||
}
|
||||
if let Some(value) = value.as_str() {
|
||||
return normalize_hermes_display_streaming_text(Some(value.to_string()), strict, key);
|
||||
}
|
||||
}
|
||||
normalize_hermes_display_streaming_text(None, strict, key)
|
||||
}
|
||||
|
||||
fn normalize_hermes_display_streaming_json(
|
||||
value: Option<&Value>,
|
||||
strict: bool,
|
||||
key: &str,
|
||||
) -> Result<String, String> {
|
||||
if let Some(value) = value {
|
||||
if let Some(value) = value.as_bool() {
|
||||
return Ok(if value { "true" } else { "false" }.to_string());
|
||||
}
|
||||
if let Some(value) = value.as_str() {
|
||||
return normalize_hermes_display_streaming_text(Some(value.to_string()), strict, key);
|
||||
}
|
||||
}
|
||||
normalize_hermes_display_streaming_text(None, strict, key)
|
||||
}
|
||||
|
||||
fn yaml_key(key: &str) -> serde_yaml::Value {
|
||||
serde_yaml::Value::String(key.to_string())
|
||||
}
|
||||
@@ -2350,6 +2425,79 @@ fn insert_hermes_home_channel_if_present(
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_hermes_channel_display_fields(
|
||||
form: &mut serde_json::Map<String, Value>,
|
||||
config: &serde_yaml::Value,
|
||||
platform: &str,
|
||||
) {
|
||||
let display = config
|
||||
.as_mapping()
|
||||
.and_then(|map| yaml_get_mapping(map, "display"));
|
||||
let platform_display = display
|
||||
.and_then(|map| yaml_get_mapping(map, "platforms"))
|
||||
.and_then(|map| yaml_get_mapping(map, platform));
|
||||
let legacy_tool_progress = display
|
||||
.and_then(|map| yaml_get_mapping(map, "tool_progress_overrides"))
|
||||
.and_then(|map| yaml_string_field(map, platform));
|
||||
let tool_progress = normalize_hermes_display_tool_progress(
|
||||
platform_display
|
||||
.and_then(|map| yaml_string_field(map, "tool_progress"))
|
||||
.or(legacy_tool_progress)
|
||||
.or_else(|| display.and_then(|map| yaml_string_field(map, "tool_progress"))),
|
||||
false,
|
||||
"display.tool_progress",
|
||||
)
|
||||
.unwrap_or_else(|_| "all".to_string());
|
||||
let show_reasoning = platform_display
|
||||
.and_then(|map| yaml_bool_field(map, "show_reasoning"))
|
||||
.or_else(|| display.and_then(|map| yaml_bool_field(map, "show_reasoning")))
|
||||
.unwrap_or(false);
|
||||
let tool_preview_length = bounded_hermes_i64(
|
||||
platform_display
|
||||
.and_then(|map| yaml_i64_field(map, "tool_preview_length"))
|
||||
.or_else(|| display.and_then(|map| yaml_i64_field(map, "tool_preview_length"))),
|
||||
0,
|
||||
0,
|
||||
200000,
|
||||
);
|
||||
let streaming = if let Some(platform_display) = platform_display {
|
||||
if let Some(value) = yaml_get(platform_display, "streaming") {
|
||||
normalize_hermes_display_streaming_yaml(
|
||||
Some(value),
|
||||
false,
|
||||
"display.platforms.streaming",
|
||||
)
|
||||
.unwrap_or_else(|_| "inherit".to_string())
|
||||
} else {
|
||||
"inherit".to_string()
|
||||
}
|
||||
} else {
|
||||
"inherit".to_string()
|
||||
};
|
||||
let cleanup_progress = platform_display
|
||||
.and_then(|map| yaml_bool_field(map, "cleanup_progress"))
|
||||
.or_else(|| display.and_then(|map| yaml_bool_field(map, "cleanup_progress")))
|
||||
.unwrap_or(false);
|
||||
|
||||
form.insert(
|
||||
"displayToolProgress".to_string(),
|
||||
Value::String(tool_progress),
|
||||
);
|
||||
form.insert(
|
||||
"displayShowReasoning".to_string(),
|
||||
Value::Bool(show_reasoning),
|
||||
);
|
||||
form.insert(
|
||||
"displayToolPreviewLength".to_string(),
|
||||
Value::Number(tool_preview_length.into()),
|
||||
);
|
||||
form.insert("displayStreaming".to_string(), Value::String(streaming));
|
||||
form.insert(
|
||||
"displayCleanupProgress".to_string(),
|
||||
Value::Bool(cleanup_progress),
|
||||
);
|
||||
}
|
||||
|
||||
fn build_hermes_channel_config_values(
|
||||
config: &serde_yaml::Value,
|
||||
env_values: &std::collections::HashMap<String, String>,
|
||||
@@ -2766,6 +2914,7 @@ fn build_hermes_channel_config_values(
|
||||
insert_json_csv_if_present(&mut form, &extra, "allow_from", "allowFrom");
|
||||
insert_json_csv_if_present(&mut form, &extra, "group_allow_from", "groupAllowFrom");
|
||||
}
|
||||
insert_hermes_channel_display_fields(&mut form, config, platform);
|
||||
values.insert(platform.to_string(), Value::Object(form));
|
||||
}
|
||||
|
||||
@@ -2950,6 +3099,95 @@ fn set_hermes_home_channel(entry: &mut serde_yaml::Mapping, form: &Value) {
|
||||
entry.insert(yaml_key("home_channel"), serde_yaml::Value::Mapping(home));
|
||||
}
|
||||
|
||||
fn merge_hermes_channel_display_config(
|
||||
root: &mut serde_yaml::Mapping,
|
||||
platform: &str,
|
||||
form: &Value,
|
||||
) -> Result<(), String> {
|
||||
let has_display_fields = [
|
||||
"displayToolProgress",
|
||||
"displayShowReasoning",
|
||||
"displayToolPreviewLength",
|
||||
"displayStreaming",
|
||||
"displayCleanupProgress",
|
||||
]
|
||||
.iter()
|
||||
.any(|key| form.get(*key).is_some());
|
||||
if !has_display_fields {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let tool_progress = if form.get("displayToolProgress").is_some() {
|
||||
Some(normalize_hermes_display_tool_progress(
|
||||
form_string(form, "displayToolProgress"),
|
||||
true,
|
||||
&format!("display.platforms.{platform}.tool_progress"),
|
||||
)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let show_reasoning = if form.get("displayShowReasoning").is_some() {
|
||||
Some(form_bool(form, "displayShowReasoning").unwrap_or(false))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let tool_preview_length = if form.get("displayToolPreviewLength").is_some() {
|
||||
Some(validate_hermes_i64(
|
||||
form_i64(form, "displayToolPreviewLength"),
|
||||
&format!("display.platforms.{platform}.tool_preview_length"),
|
||||
0,
|
||||
0,
|
||||
200000,
|
||||
)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let streaming = if form.get("displayStreaming").is_some() {
|
||||
Some(normalize_hermes_display_streaming_json(
|
||||
form.get("displayStreaming"),
|
||||
true,
|
||||
&format!("display.platforms.{platform}.streaming"),
|
||||
)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let cleanup_progress = if form.get("displayCleanupProgress").is_some() {
|
||||
Some(form_bool(form, "displayCleanupProgress").unwrap_or(false))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let display = yaml_child_object(root, "display")?;
|
||||
let platforms = yaml_child_object(display, "platforms")?;
|
||||
let platform_display = yaml_child_object(platforms, platform)?;
|
||||
if let Some(value) = tool_progress {
|
||||
platform_display.insert(yaml_key("tool_progress"), serde_yaml::Value::String(value));
|
||||
}
|
||||
if let Some(value) = show_reasoning {
|
||||
platform_display.insert(yaml_key("show_reasoning"), serde_yaml::Value::Bool(value));
|
||||
}
|
||||
if let Some(value) = tool_preview_length {
|
||||
platform_display.insert(
|
||||
yaml_key("tool_preview_length"),
|
||||
serde_yaml::Value::Number(value.into()),
|
||||
);
|
||||
}
|
||||
if let Some(value) = streaming {
|
||||
if value == "inherit" {
|
||||
platform_display.remove(yaml_key("streaming"));
|
||||
} else {
|
||||
platform_display.insert(
|
||||
yaml_key("streaming"),
|
||||
serde_yaml::Value::Bool(value == "true"),
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(value) = cleanup_progress {
|
||||
platform_display.insert(yaml_key("cleanup_progress"), serde_yaml::Value::Bool(value));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn split_csv_items(value: &str) -> Vec<String> {
|
||||
value
|
||||
.split([',', ';', '\n'])
|
||||
@@ -3660,6 +3898,7 @@ fn merge_hermes_channel_config(
|
||||
let platform = normalize_hermes_channel_platform(platform)
|
||||
.ok_or_else(|| format!("不支持的 Hermes 渠道: {platform}"))?;
|
||||
let root = ensure_yaml_object(config)?;
|
||||
merge_hermes_channel_display_config(root, platform, form)?;
|
||||
let platforms = yaml_child_object(root, "platforms")?;
|
||||
let entry = yaml_child_object(platforms, platform)?;
|
||||
|
||||
@@ -10677,4 +10916,158 @@ platforms:
|
||||
assert!(env.contains(&("LINE_ALLOWED_GROUPS".to_string(), "C1".to_string())));
|
||||
assert!(env.contains(&("LINE_HOME_CHANNEL".to_string(), "U-home".to_string())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_display_values_read_platform_overrides_and_legacy_fallback() {
|
||||
let config: serde_yaml::Value = serde_yaml::from_str(
|
||||
r#"
|
||||
display:
|
||||
tool_progress: all
|
||||
show_reasoning: false
|
||||
cleanup_progress: false
|
||||
tool_progress_overrides:
|
||||
discord: off
|
||||
platforms:
|
||||
telegram:
|
||||
tool_progress: new
|
||||
show_reasoning: true
|
||||
tool_preview_length: 80
|
||||
streaming: false
|
||||
cleanup_progress: true
|
||||
custom_flag: keep-me
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let values = build_hermes_channel_config_values(&config, &HashMap::new());
|
||||
|
||||
assert_eq!(values["telegram"]["displayToolProgress"], "new");
|
||||
assert_eq!(values["telegram"]["displayShowReasoning"], true);
|
||||
assert_eq!(values["telegram"]["displayToolPreviewLength"], 80);
|
||||
assert_eq!(values["telegram"]["displayStreaming"], "false");
|
||||
assert_eq!(values["telegram"]["displayCleanupProgress"], true);
|
||||
assert_eq!(values["discord"]["displayToolProgress"], "off");
|
||||
assert_eq!(values["discord"]["displayStreaming"], "inherit");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_channel_display_writes_platform_overrides_and_preserves_unknown_fields() {
|
||||
let mut config: serde_yaml::Value = serde_yaml::from_str(
|
||||
r#"
|
||||
display:
|
||||
tool_progress: all
|
||||
tool_progress_overrides:
|
||||
telegram: off
|
||||
platforms:
|
||||
telegram:
|
||||
tool_progress: new
|
||||
streaming: false
|
||||
custom_flag: keep-me
|
||||
runtime_footer:
|
||||
enabled: true
|
||||
platforms:
|
||||
telegram:
|
||||
enabled: true
|
||||
extra:
|
||||
unknown_option: keep-platform
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
merge_hermes_channel_config(
|
||||
&mut config,
|
||||
"telegram",
|
||||
&json!({
|
||||
"enabled": true,
|
||||
"botToken": "",
|
||||
"displayToolProgress": "verbose",
|
||||
"displayShowReasoning": false,
|
||||
"displayToolPreviewLength": "120",
|
||||
"displayStreaming": "inherit",
|
||||
"displayCleanupProgress": false,
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(config["display"]["tool_progress"].as_str(), Some("all"));
|
||||
assert_eq!(
|
||||
config["display"]["tool_progress_overrides"]["telegram"].as_str(),
|
||||
Some("off")
|
||||
);
|
||||
assert_eq!(
|
||||
config["display"]["platforms"]["telegram"]["tool_progress"].as_str(),
|
||||
Some("verbose")
|
||||
);
|
||||
assert_eq!(
|
||||
config["display"]["platforms"]["telegram"]["show_reasoning"].as_bool(),
|
||||
Some(false)
|
||||
);
|
||||
assert_eq!(
|
||||
config["display"]["platforms"]["telegram"]["tool_preview_length"].as_i64(),
|
||||
Some(120)
|
||||
);
|
||||
assert_eq!(
|
||||
config["display"]["platforms"]["telegram"]["streaming"],
|
||||
serde_yaml::Value::Null
|
||||
);
|
||||
assert_eq!(
|
||||
config["display"]["platforms"]["telegram"]["cleanup_progress"].as_bool(),
|
||||
Some(false)
|
||||
);
|
||||
assert_eq!(
|
||||
config["display"]["platforms"]["telegram"]["custom_flag"].as_str(),
|
||||
Some("keep-me")
|
||||
);
|
||||
assert_eq!(
|
||||
config["display"]["platforms"]["telegram"]["runtime_footer"]["enabled"].as_bool(),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
config["platforms"]["telegram"]["extra"]["unknown_option"].as_str(),
|
||||
Some("keep-platform")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_channel_display_rejects_invalid_values() {
|
||||
let mut config = serde_yaml::Value::Mapping(serde_yaml::Mapping::new());
|
||||
let err = merge_hermes_channel_config(
|
||||
&mut config,
|
||||
"telegram",
|
||||
&json!({
|
||||
"enabled": true,
|
||||
"displayToolProgress": "everything",
|
||||
"displayToolPreviewLength": 80,
|
||||
"displayStreaming": "inherit",
|
||||
}),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(err.contains("display.platforms.telegram.tool_progress"));
|
||||
|
||||
let err = merge_hermes_channel_config(
|
||||
&mut config,
|
||||
"telegram",
|
||||
&json!({
|
||||
"enabled": true,
|
||||
"displayToolProgress": "all",
|
||||
"displayToolPreviewLength": 200001,
|
||||
"displayStreaming": "inherit",
|
||||
}),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(err.contains("display.platforms.telegram.tool_preview_length"));
|
||||
|
||||
let err = merge_hermes_channel_config(
|
||||
&mut config,
|
||||
"telegram",
|
||||
&json!({
|
||||
"enabled": true,
|
||||
"displayToolProgress": "all",
|
||||
"displayToolPreviewLength": 80,
|
||||
"displayStreaming": "global",
|
||||
}),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(err.contains("display.platforms.telegram.streaming"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,6 +197,36 @@ const LEGACY_POLICY_FIELDS = [
|
||||
{ key: 'groupAllowFrom', labelKey: 'engine.hermesChannelGroupAllowFrom', type: 'textarea', placeholderKey: 'engine.hermesChannelGroupAllowFromPlaceholder' },
|
||||
]
|
||||
|
||||
const DISPLAY_FIELDS = [
|
||||
{
|
||||
key: 'displayToolProgress',
|
||||
labelKey: 'engine.hermesChannelDisplayToolProgress',
|
||||
type: 'select',
|
||||
options: [
|
||||
['off', 'engine.hermesChannelDisplayToolProgressOff'],
|
||||
['new', 'engine.hermesChannelDisplayToolProgressNew'],
|
||||
['all', 'engine.hermesChannelDisplayToolProgressAll'],
|
||||
['verbose', 'engine.hermesChannelDisplayToolProgressVerbose'],
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'displayStreaming',
|
||||
labelKey: 'engine.hermesChannelDisplayStreaming',
|
||||
type: 'select',
|
||||
options: [
|
||||
['inherit', 'engine.hermesChannelDisplayStreamingInherit'],
|
||||
['true', 'engine.hermesChannelDisplayStreamingOn'],
|
||||
['false', 'engine.hermesChannelDisplayStreamingOff'],
|
||||
],
|
||||
},
|
||||
{ key: 'displayToolPreviewLength', labelKey: 'engine.hermesChannelDisplayToolPreviewLength', type: 'number', placeholder: '0' },
|
||||
]
|
||||
|
||||
const DISPLAY_TOGGLES = [
|
||||
{ key: 'displayShowReasoning', labelKey: 'engine.hermesChannelDisplayShowReasoning', type: 'checkbox' },
|
||||
{ key: 'displayCleanupProgress', labelKey: 'engine.hermesChannelDisplayCleanupProgress', type: 'checkbox' },
|
||||
]
|
||||
|
||||
function esc(value) {
|
||||
return String(value ?? '')
|
||||
.replace(/&/g, '&')
|
||||
@@ -211,7 +241,14 @@ function channelMeta(id) {
|
||||
|
||||
function defaultForm(platform) {
|
||||
const channel = channelMeta(platform)
|
||||
const form = { enabled: false }
|
||||
const form = {
|
||||
enabled: false,
|
||||
displayToolProgress: 'all',
|
||||
displayShowReasoning: false,
|
||||
displayToolPreviewLength: 0,
|
||||
displayStreaming: 'inherit',
|
||||
displayCleanupProgress: false,
|
||||
}
|
||||
if (!channel.policyFields) {
|
||||
form.dmPolicy = 'pair'
|
||||
form.groupPolicy = 'allowlist'
|
||||
@@ -421,6 +458,19 @@ export function render() {
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="hm-channel-section">
|
||||
<div>
|
||||
<div class="hm-channel-section-title">${esc(t('engine.hermesChannelDisplayBehavior'))}</div>
|
||||
<div class="hm-channel-section-hint">${esc(t('engine.hermesChannelDisplayHint'))}</div>
|
||||
</div>
|
||||
<div class="hm-field-row">
|
||||
${DISPLAY_FIELDS.map(field => renderField(field, form, disabled)).join('')}
|
||||
</div>
|
||||
<div class="hm-channel-toggle-grid">
|
||||
${DISPLAY_TOGGLES.map(field => renderField(field, form, disabled)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${(channel.advancedFields || []).length ? `
|
||||
<div class="hm-channel-section">
|
||||
<div class="hm-channel-section-title">${esc(t('engine.hermesChannelRuntimeBehavior'))}</div>
|
||||
|
||||
@@ -6849,6 +6849,13 @@ body[data-active-engine="hermes"][data-theme="dark"] {
|
||||
font-weight: 500;
|
||||
color: var(--hm-text-primary);
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-section-hint {
|
||||
margin-top: 6px;
|
||||
color: var(--hm-text-tertiary);
|
||||
font-size: 12.5px;
|
||||
line-height: 1.6;
|
||||
max-width: 760px;
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-textarea {
|
||||
min-height: 104px;
|
||||
height: auto;
|
||||
|
||||
@@ -1110,6 +1110,20 @@ export default {
|
||||
hermesChannelAllowFromPlaceholder: _('每行或逗号分隔一个用户 ID,开放策略可留空。', 'One user ID per line or comma-separated. Leave empty for open policy.', '每行或逗號分隔一個使用者 ID,開放策略可留空。'),
|
||||
hermesChannelGroupAllowFromPlaceholder: _('每行或逗号分隔一个群组 / 频道 ID。', 'One group or channel ID per line or comma-separated.', '每行或逗號分隔一個群組 / 頻道 ID。'),
|
||||
hermesChannelRequireMention: _('群组消息需要 @Bot 才响应', 'Require @mention in groups', '群組訊息需要 @Bot 才回應'),
|
||||
hermesChannelDisplayBehavior: _('显示与进度', 'Display & Progress', '顯示與進度'),
|
||||
hermesChannelDisplayHint: _('这些设置只影响当前渠道的推理展示、工具进度和流式输出覆盖;“跟随全局”不会写入平台级流式开关。', 'These settings only affect reasoning display, tool progress, and streaming override for the current platform. “Inherit global” does not write a platform streaming override.', '這些設定只影響目前頻道的推理顯示、工具進度和串流輸出覆蓋;「跟隨全域」不會寫入平台級串流開關。'),
|
||||
hermesChannelDisplayToolProgress: _('工具进度', 'Tool Progress', '工具進度'),
|
||||
hermesChannelDisplayToolProgressOff: _('关闭', 'Off', '關閉'),
|
||||
hermesChannelDisplayToolProgressNew: _('仅新工具', 'New tools only', '僅新工具'),
|
||||
hermesChannelDisplayToolProgressAll: _('全部工具', 'All tools', '全部工具'),
|
||||
hermesChannelDisplayToolProgressVerbose: _('详细', 'Verbose', '詳細'),
|
||||
hermesChannelDisplayStreaming: _('流式输出', 'Streaming', '串流輸出'),
|
||||
hermesChannelDisplayStreamingInherit: _('跟随全局', 'Inherit global', '跟隨全域'),
|
||||
hermesChannelDisplayStreamingOn: _('开启', 'On', '開啟'),
|
||||
hermesChannelDisplayStreamingOff: _('关闭', 'Off', '關閉'),
|
||||
hermesChannelDisplayToolPreviewLength: _('工具预览长度', 'Tool Preview Length', '工具預覽長度'),
|
||||
hermesChannelDisplayShowReasoning: _('显示推理过程', 'Show reasoning', '顯示推理過程'),
|
||||
hermesChannelDisplayCleanupProgress: _('完成后清理进度', 'Clean up progress after completion', '完成後清理進度'),
|
||||
hermesChannelRestartHint: _('保存会将访问策略等偏好写入 config.yaml,并将 Bot Token、App Secret、Client Secret 及 Hermes 运行时兼容环境变量同步到 .env。Hermes Gateway 读取启动时配置,修改后请重启 Gateway。', 'Saving writes access preferences to config.yaml and syncs Bot Token, App Secret, Client Secret, and Hermes runtime compatibility variables to .env. Hermes Gateway reads them on startup, so restart the gateway after changes.', '儲存會將存取策略等偏好寫入 config.yaml,並將 Bot Token、App Secret、Client Secret 及 Hermes 執行時相容環境變數同步到 .env。Hermes Gateway 於啟動時讀取設定,修改後請重啟 Gateway。'),
|
||||
extensionsEyebrow: _('HERMES AGENT · 扩展', 'HERMES AGENT · EXTENSIONS', 'HERMES AGENT · 擴展'),
|
||||
extensionsTitle: _('文档 / 插件 / 主题', 'Docs / Plugins / Themes', '文件 / 插件 / 主題'),
|
||||
|
||||
@@ -651,3 +651,105 @@ test('Hermes 插件平台保存会写入运行时读取的 YAML 字段和环境
|
||||
assert.equal(env.SIMPLEX_WS_URL, 'ws://127.0.0.1:5225')
|
||||
assert.equal(env.SIMPLEX_ALLOW_ALL_USERS, 'true')
|
||||
})
|
||||
|
||||
test('Hermes 渠道读取会回显平台级显示和进度策略', () => {
|
||||
const values = buildHermesChannelConfigValues({
|
||||
display: {
|
||||
tool_progress: 'all',
|
||||
show_reasoning: false,
|
||||
cleanup_progress: false,
|
||||
tool_progress_overrides: {
|
||||
discord: 'off',
|
||||
},
|
||||
platforms: {
|
||||
telegram: {
|
||||
tool_progress: 'new',
|
||||
show_reasoning: true,
|
||||
tool_preview_length: 80,
|
||||
streaming: false,
|
||||
cleanup_progress: true,
|
||||
custom_flag: 'keep-me',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(values.telegram.displayToolProgress, 'new')
|
||||
assert.equal(values.telegram.displayShowReasoning, true)
|
||||
assert.equal(values.telegram.displayToolPreviewLength, 80)
|
||||
assert.equal(values.telegram.displayStreaming, 'false')
|
||||
assert.equal(values.telegram.displayCleanupProgress, true)
|
||||
assert.equal(values.discord.displayToolProgress, 'off')
|
||||
assert.equal(values.discord.displayStreaming, 'inherit')
|
||||
})
|
||||
|
||||
test('Hermes 渠道保存会写入 display.platforms 平台覆盖并保留未知字段', () => {
|
||||
const next = mergeHermesChannelConfig({
|
||||
display: {
|
||||
tool_progress: 'all',
|
||||
tool_progress_overrides: {
|
||||
telegram: 'off',
|
||||
},
|
||||
platforms: {
|
||||
telegram: {
|
||||
tool_progress: 'new',
|
||||
streaming: false,
|
||||
custom_flag: 'keep-me',
|
||||
runtime_footer: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
platforms: {
|
||||
telegram: {
|
||||
enabled: true,
|
||||
extra: {
|
||||
unknown_option: 'keep-platform',
|
||||
},
|
||||
},
|
||||
},
|
||||
}, 'telegram', {
|
||||
enabled: true,
|
||||
botToken: '',
|
||||
displayToolProgress: 'verbose',
|
||||
displayShowReasoning: false,
|
||||
displayToolPreviewLength: '120',
|
||||
displayStreaming: 'inherit',
|
||||
displayCleanupProgress: false,
|
||||
})
|
||||
|
||||
assert.equal(next.display.tool_progress, 'all')
|
||||
assert.equal(next.display.tool_progress_overrides.telegram, 'off')
|
||||
assert.equal(next.display.platforms.telegram.tool_progress, 'verbose')
|
||||
assert.equal(next.display.platforms.telegram.show_reasoning, false)
|
||||
assert.equal(next.display.platforms.telegram.tool_preview_length, 120)
|
||||
assert.equal(next.display.platforms.telegram.streaming, undefined)
|
||||
assert.equal(next.display.platforms.telegram.cleanup_progress, false)
|
||||
assert.equal(next.display.platforms.telegram.custom_flag, 'keep-me')
|
||||
assert.deepEqual(next.display.platforms.telegram.runtime_footer, { enabled: true })
|
||||
assert.equal(next.platforms.telegram.extra.unknown_option, 'keep-platform')
|
||||
})
|
||||
|
||||
test('Hermes 渠道显示策略保存会拒绝无效选项和越界预览长度', () => {
|
||||
assert.throws(() => mergeHermesChannelConfig({}, 'telegram', {
|
||||
enabled: true,
|
||||
displayToolProgress: 'everything',
|
||||
displayToolPreviewLength: 80,
|
||||
displayStreaming: 'inherit',
|
||||
}), /display\.platforms\.telegram\.tool_progress/)
|
||||
|
||||
assert.throws(() => mergeHermesChannelConfig({}, 'telegram', {
|
||||
enabled: true,
|
||||
displayToolProgress: 'all',
|
||||
displayToolPreviewLength: 200001,
|
||||
displayStreaming: 'inherit',
|
||||
}), /display\.platforms\.telegram\.tool_preview_length/)
|
||||
|
||||
assert.throws(() => mergeHermesChannelConfig({}, 'telegram', {
|
||||
enabled: true,
|
||||
displayToolProgress: 'all',
|
||||
displayToolPreviewLength: 80,
|
||||
displayStreaming: 'global',
|
||||
}), /display\.platforms\.telegram\.streaming/)
|
||||
})
|
||||
|
||||
@@ -68,3 +68,34 @@ test('Hermes bundled plugin 渠道页新增平台不会暴露翻译 key', () =>
|
||||
assert.notEqual(t(key), key, `${key} 缺少运行时翻译`)
|
||||
}
|
||||
})
|
||||
|
||||
test('Hermes 渠道页会暴露平台级显示和进度策略入口', () => {
|
||||
for (const field of [
|
||||
'displayToolProgress',
|
||||
'displayShowReasoning',
|
||||
'displayToolPreviewLength',
|
||||
'displayStreaming',
|
||||
'displayCleanupProgress',
|
||||
]) {
|
||||
assert.match(source, new RegExp(`key:\\s*'${field}'`), `缺少 ${field} 显示策略字段`)
|
||||
}
|
||||
|
||||
for (const key of [
|
||||
'engine.hermesChannelDisplayBehavior',
|
||||
'engine.hermesChannelDisplayHint',
|
||||
'engine.hermesChannelDisplayToolProgress',
|
||||
'engine.hermesChannelDisplayToolProgressOff',
|
||||
'engine.hermesChannelDisplayToolProgressNew',
|
||||
'engine.hermesChannelDisplayToolProgressAll',
|
||||
'engine.hermesChannelDisplayToolProgressVerbose',
|
||||
'engine.hermesChannelDisplayStreaming',
|
||||
'engine.hermesChannelDisplayStreamingInherit',
|
||||
'engine.hermesChannelDisplayStreamingOn',
|
||||
'engine.hermesChannelDisplayStreamingOff',
|
||||
'engine.hermesChannelDisplayToolPreviewLength',
|
||||
'engine.hermesChannelDisplayShowReasoning',
|
||||
'engine.hermesChannelDisplayCleanupProgress',
|
||||
]) {
|
||||
assert.notEqual(t(key), key, `${key} 缺少运行时翻译`)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user