feat(hermes): add display tool prefix config

This commit is contained in:
晴天
2026-05-26 23:37:38 +08:00
parent 466e6c8831
commit 842cf83917
6 changed files with 73 additions and 1 deletions

View File

@@ -3599,6 +3599,13 @@ function normalizeHermesDisplayToolProgress(value, strict = false, key = 'displa
return 'all'
}
function normalizeHermesDisplayToolPrefix(value, strict = false) {
const prefix = String(value ?? '').trim() || '┊'
if (prefix.length <= 8 && !/[\r\n\t]/.test(prefix)) return prefix
if (strict) throw new Error('display.tool_prefix 必须是 1 到 8 个字符,且不能包含换行或制表符')
return '┊'
}
function normalizeHermesDisplayStreaming(value, strict = false, key = 'display.streaming') {
if (typeof value === 'boolean') return value ? 'true' : 'false'
const streaming = String(value ?? '').trim().toLowerCase() || 'inherit'
@@ -3687,6 +3694,7 @@ export function buildHermesDisplayConfigValues(config = {}) {
return {
displayCompact: readHermesBool(display.compact, false),
displaySkin: normalizeHermesDisplaySkin(display.skin, false),
displayToolPrefix: normalizeHermesDisplayToolPrefix(display.tool_prefix, false),
displayToolProgress: normalizeHermesDisplayToolProgress(display.tool_progress, false),
displayShowReasoning: readHermesBool(display.show_reasoning, false),
displayToolPreviewLength: parseHermesInteger(display.tool_preview_length, 'display.tool_preview_length', 0, 0, 200000, false),
@@ -3720,6 +3728,7 @@ export function mergeHermesDisplayConfig(config = {}, form = {}) {
display.compact = formHermesBool(form, 'displayCompact', currentValues.displayCompact)
display.skin = normalizeHermesDisplaySkin(Object.hasOwn(form, 'displaySkin') ? form.displaySkin : currentValues.displaySkin, true)
display.tool_prefix = normalizeHermesDisplayToolPrefix(Object.hasOwn(form, 'displayToolPrefix') ? form.displayToolPrefix : currentValues.displayToolPrefix, true)
display.tool_progress = normalizeHermesDisplayToolProgress(Object.hasOwn(form, 'displayToolProgress') ? form.displayToolProgress : currentValues.displayToolProgress, true, 'display.tool_progress')
display.show_reasoning = formHermesBool(form, 'displayShowReasoning', currentValues.displayShowReasoning)
display.tool_preview_length = parseHermesInteger(Object.hasOwn(form, 'displayToolPreviewLength') ? form.displayToolPreviewLength : currentValues.displayToolPreviewLength, 'display.tool_preview_length', 0, 0, 200000, true)

View File

@@ -2209,6 +2209,25 @@ fn normalize_hermes_display_tool_progress(
}
}
fn normalize_hermes_display_tool_prefix(
value: Option<String>,
strict: bool,
) -> Result<String, String> {
let prefix = value.unwrap_or_default().trim().to_string();
let prefix = if prefix.is_empty() {
"".to_string()
} else {
prefix
};
if prefix.chars().count() <= 8 && !prefix.contains(['\r', '\n', '\t']) {
Ok(prefix)
} else if strict {
Err("display.tool_prefix 必须是 1 到 8 个字符,且不能包含换行或制表符".to_string())
} else {
Ok("".to_string())
}
}
fn normalize_hermes_display_streaming_text(
value: Option<String>,
strict: bool,
@@ -6232,6 +6251,10 @@ fn build_hermes_display_config_values(config: &serde_yaml::Value) -> Value {
display.and_then(|map| yaml_string_field(map, "skin")),
false,
).unwrap_or_else(|_| "default".to_string()),
"displayToolPrefix": normalize_hermes_display_tool_prefix(
display.and_then(|map| yaml_string_field(map, "tool_prefix")),
false,
).unwrap_or_else(|_| "".to_string()),
"displayToolProgress": normalize_hermes_display_tool_progress(
display.and_then(|map| yaml_string_field(map, "tool_progress")),
false,
@@ -6338,6 +6361,17 @@ fn merge_hermes_display_config(config: &mut serde_yaml::Value, form: &Value) ->
true,
)?),
);
display.insert(
yaml_key("tool_prefix"),
serde_yaml::Value::String(normalize_hermes_display_tool_prefix(
form_string(form, "displayToolPrefix").or_else(|| {
current["displayToolPrefix"]
.as_str()
.map(ToString::to_string)
}),
true,
)?),
);
display.insert(
yaml_key("tool_progress"),
serde_yaml::Value::String(tool_progress),
@@ -18958,6 +18992,7 @@ mod hermes_display_config_tests {
assert_eq!(values["displayToolProgress"], "all");
assert_eq!(values["displayCompact"], false);
assert_eq!(values["displaySkin"], "default");
assert_eq!(values["displayToolPrefix"], "");
assert_eq!(values["displayShowReasoning"], false);
assert_eq!(values["displayToolPreviewLength"], 0);
assert_eq!(values["displayCleanupProgress"], false);
@@ -18988,6 +19023,7 @@ display:
tool_progress: VERBOSE
compact: true
skin: MONO
tool_prefix: "╎"
show_reasoning: true
tool_preview_length: 80
cleanup_progress: true
@@ -19016,6 +19052,7 @@ display:
assert_eq!(values["displayToolProgress"], "verbose");
assert_eq!(values["displayCompact"], true);
assert_eq!(values["displaySkin"], "mono");
assert_eq!(values["displayToolPrefix"], "");
assert_eq!(values["displayShowReasoning"], true);
assert_eq!(values["displayToolPreviewLength"], 80);
assert_eq!(values["displayCleanupProgress"], true);
@@ -19064,6 +19101,7 @@ memory:
"displayToolProgress": "off",
"displayCompact": true,
"displaySkin": "slate",
"displayToolPrefix": "",
"displayShowReasoning": true,
"displayToolPreviewLength": 120,
"displayCleanupProgress": true,
@@ -19089,6 +19127,7 @@ memory:
assert_eq!(config["memory"]["memory_enabled"].as_bool(), Some(true));
assert_eq!(config["display"]["compact"].as_bool(), Some(true));
assert_eq!(config["display"]["skin"].as_str(), Some("slate"));
assert_eq!(config["display"]["tool_prefix"].as_str(), Some(""));
assert_eq!(config["display"]["show_reasoning"].as_bool(), Some(true));
assert_eq!(config["display"]["tool_preview_length"].as_i64(), Some(120));
assert_eq!(config["display"]["cleanup_progress"].as_bool(), Some(true));
@@ -19166,6 +19205,13 @@ memory:
.unwrap_err();
assert!(err.contains("display.skin"));
let err = merge_hermes_display_config(
&mut config,
&json!({ "displayToolPrefix": "too-long-prefix" }),
)
.unwrap_err();
assert!(err.contains("display.tool_prefix"));
let err =
merge_hermes_display_config(&mut config, &json!({ "displayResumeDisplay": "compact" }))
.unwrap_err();

View File

@@ -147,6 +147,7 @@ const DISPLAY_DEFAULTS = {
displayToolProgress: 'all',
displayCompact: false,
displaySkin: 'default',
displayToolPrefix: '┊',
displayShowReasoning: false,
displayToolPreviewLength: 0,
displayCleanupProgress: false,
@@ -1327,6 +1328,10 @@ export function render() {
${DISPLAY_SKINS.map(mode => option(`engine.hermesDisplayConfigSkin_${mode}`, mode, displayValues.displaySkin)).join('')}
</select>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesDisplayConfigToolPrefix')}</span>
<input id="hm-display-tool-prefix" class="hm-input" maxlength="8" value="${esc(displayValues.displayToolPrefix)}" ${disabled ? 'disabled' : ''}>
</label>
<label class="hm-field">
<span class="hm-field-label">${t('engine.hermesDisplayConfigLanguage')}</span>
<select id="hm-display-language" class="hm-input" ${disabled ? 'disabled' : ''}>
@@ -3355,6 +3360,7 @@ export function render() {
displayToolProgress: el.querySelector('#hm-display-tool-progress')?.value || 'all',
displayCompact: !!el.querySelector('#hm-display-compact')?.checked,
displaySkin: el.querySelector('#hm-display-skin')?.value || 'default',
displayToolPrefix: el.querySelector('#hm-display-tool-prefix')?.value || '┊',
displayShowReasoning: !!el.querySelector('#hm-display-show-reasoning')?.checked,
displayToolPreviewLength: el.querySelector('#hm-display-tool-preview-length')?.value || '0',
displayCleanupProgress: !!el.querySelector('#hm-display-cleanup-progress')?.checked,

View File

@@ -945,7 +945,7 @@ export default {
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 的默认进度展示、工具预览、推理展示、进度清理、横幅紧凑模式、显示皮肤、最终回复 Markdown、时间戳、完成提醒、终端输出恢复、忙时输入、后台进程通知、静态提示语言、运行信息页脚以及文件写入失败校验。', 'Control default progress display, tool previews, reasoning visibility, progress cleanup, compact banner mode, display skin, final-response Markdown, timestamps, completion bell, terminal output recovery, busy input handling, background process notifications, static prompt language, runtime footer, and failed file-mutation verification for messaging platforms and CLI.', '控制訊息平台和 CLI 的預設進度顯示、工具預覽、推理展示、進度清理、橫幅緊湊模式、顯示皮膚、最終回覆 Markdown、時間戳、完成提醒、終端輸出恢復、忙時輸入、背景程序通知、靜態提示語言、執行資訊頁腳以及檔案寫入失敗校驗。'),
hermesDisplayConfigDesc: _('控制消息平台和 CLI 的默认进度展示、工具预览、工具输出前缀、推理展示、进度清理、横幅紧凑模式、显示皮肤、最终回复 Markdown、时间戳、完成提醒、终端输出恢复、忙时输入、后台进程通知、静态提示语言、运行信息页脚以及文件写入失败校验。', 'Control default progress display, tool previews, tool output prefix, reasoning visibility, progress cleanup, compact banner mode, display skin, final-response Markdown, timestamps, completion bell, terminal output recovery, busy input handling, background process notifications, static prompt language, runtime footer, and failed file-mutation verification for messaging platforms and CLI.', '控制訊息平台和 CLI 的預設進度顯示、工具預覽、工具輸出前綴、推理展示、進度清理、橫幅緊湊模式、顯示皮膚、最終回覆 Markdown、時間戳、完成提醒、終端輸出恢復、忙時輸入、背景程序通知、靜態提示語言、執行資訊頁腳以及檔案寫入失敗校驗。'),
hermesDisplayConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'),
hermesDisplayConfigSave: _('保存显示设置', 'Save display settings', '儲存顯示設定'),
hermesDisplayConfigSaveSuccess: _('显示与可靠性配置已保存,建议重启 Hermes Gateway 生效', 'Display and reliability settings saved. Restart Hermes Gateway to take effect.', '顯示與可靠性設定已儲存,建議重啟 Hermes Gateway 生效'),
@@ -958,6 +958,7 @@ export default {
hermesDisplayConfigToolProgress_verbose: _('详细显示参数和结果', 'Verbose args and results', '詳細顯示參數與結果'),
hermesDisplayConfigCompact: _('使用紧凑启动横幅', 'Use compact startup banner', '使用緊湊啟動橫幅'),
hermesDisplayConfigSkin: _('CLI 显示皮肤', 'CLI display skin', 'CLI 顯示皮膚'),
hermesDisplayConfigToolPrefix: _('工具输出前缀', 'Tool output prefix', '工具輸出前綴'),
hermesDisplayConfigSkin_default: _('默认', 'Default', '預設'),
hermesDisplayConfigSkin_ares: _('Ares', 'Ares', 'Ares'),
hermesDisplayConfigSkin_mono: _('Mono', 'Mono', 'Mono'),

View File

@@ -191,6 +191,7 @@ test('Hermes 配置页会暴露全局显示与可靠性结构化配置字段', (
'hm-display-tool-progress',
'hm-display-compact',
'hm-display-skin',
'hm-display-tool-prefix',
'hm-display-show-reasoning',
'hm-display-tool-preview-length',
'hm-display-cleanup-progress',

View File

@@ -13,6 +13,7 @@ test('Hermes 显示配置读取会提供上游默认值', () => {
displayToolProgress: 'all',
displayCompact: false,
displaySkin: 'default',
displayToolPrefix: '┊',
displayShowReasoning: false,
displayToolPreviewLength: 0,
displayCleanupProgress: false,
@@ -39,6 +40,7 @@ test('Hermes 显示配置读取会规范化已有字段', () => {
tool_progress: 'VERBOSE',
compact: true,
skin: 'MONO',
tool_prefix: '╎',
show_reasoning: true,
tool_preview_length: 80,
cleanup_progress: true,
@@ -64,6 +66,7 @@ test('Hermes 显示配置读取会规范化已有字段', () => {
assert.equal(values.displayToolProgress, 'verbose')
assert.equal(values.displayCompact, true)
assert.equal(values.displaySkin, 'mono')
assert.equal(values.displayToolPrefix, '╎')
assert.equal(values.displayShowReasoning, true)
assert.equal(values.displayToolPreviewLength, 80)
assert.equal(values.displayCleanupProgress, true)
@@ -101,6 +104,7 @@ test('Hermes 显示配置保存会保留未知 YAML 并写入 display', () => {
displayToolProgress: 'off',
displayCompact: true,
displaySkin: 'slate',
displayToolPrefix: '│',
displayShowReasoning: true,
displayToolPreviewLength: 120,
displayCleanupProgress: true,
@@ -124,6 +128,7 @@ test('Hermes 显示配置保存会保留未知 YAML 并写入 display', () => {
assert.deepEqual(next.memory, { memory_enabled: true })
assert.equal(next.display.compact, true)
assert.equal(next.display.skin, 'slate')
assert.equal(next.display.tool_prefix, '│')
assert.equal(next.display.show_reasoning, true)
assert.equal(next.display.tool_preview_length, 120)
assert.equal(next.display.cleanup_progress, true)
@@ -155,6 +160,10 @@ test('Hermes 显示配置保存会拒绝非法枚举和页脚字段', () => {
() => mergeHermesDisplayConfig({}, { displaySkin: 'unknown' }),
/display\.skin/,
)
assert.throws(
() => mergeHermesDisplayConfig({}, { displayToolPrefix: 'too-long-prefix' }),
/display\.tool_prefix/,
)
assert.throws(
() => mergeHermesDisplayConfig({}, { displayResumeDisplay: 'compact' }),
/display\.resume_display/,